From ef6d21b94ee21c2f9c39df72de07a5a818ccd45a Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Tue, 16 Jun 2026 11:31:02 -0400 Subject: [PATCH] =?UTF-8?q?refactor(public):=20retire=20track-cardinal=20s?= =?UTF-8?q?tack,=20fold=20Archive/Cuts=20cards=20into=20ReleaseGallery=20(?= =?UTF-8?q?P11=20W3=20=C2=A74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AudioPlayerBar/TrackMetaLabel.razor.css | 4 +- .../Controls/DeepDrftHero.razor | 2 +- .../Controls/GalleryViewMode.cs | 7 - .../Controls/PlayStateIcon.razor.cs | 2 +- .../Controls/ReleaseDetailScaffold.razor | 2 +- .../Controls/ReleaseDetailScaffold.razor.cs | 4 +- .../Controls/ReleaseGallery.razor | 40 +++- .../Controls/TrackCard.razor | 213 ------------------ .../Controls/TrackCard.razor.cs | 34 --- .../Controls/TrackCard.razor.css | 197 ---------------- .../Controls/TracksGallery.razor | 36 --- .../Controls/TracksGallery.razor.cs | 26 --- .../Controls/TracksGallery.razor.css | 15 -- DeepDrftPublic.Client/Layout/Pages.cs | 4 +- DeepDrftPublic.Client/Pages/AlbumsView.razor | 67 +----- .../Pages/AlbumsView.razor.cs | 10 +- .../Pages/AlbumsView.razor.css | 58 ----- DeepDrftPublic.Client/Pages/ArchiveView.razor | 65 +----- .../Pages/ArchiveView.razor.cs | 6 +- .../Pages/ArchiveView.razor.css | 60 ----- DeepDrftPublic.Client/Pages/Home.razor | 2 +- .../Pages/MediumBrowseBase.cs | 2 +- .../Pages/ReleaseDetailBase.cs | 2 +- DeepDrftPublic.Client/Pages/TrackDetail.razor | 101 --------- .../Pages/TrackDetail.razor.cs | 70 ------ DeepDrftPublic.Client/Pages/TracksView.razor | 92 -------- .../Pages/TracksView.razor.cs | 163 -------------- .../Pages/TracksView.razor.css | 45 ---- DeepDrftPublic.Client/Startup.cs | 2 - .../ViewModels/ReleaseDetailViewModel.cs | 2 +- .../ViewModels/TrackDetailViewModel.cs | 45 ---- .../ViewModels/TracksViewModel.cs | 45 ---- 32 files changed, 70 insertions(+), 1353 deletions(-) delete mode 100644 DeepDrftPublic.Client/Controls/GalleryViewMode.cs delete mode 100644 DeepDrftPublic.Client/Controls/TrackCard.razor delete mode 100644 DeepDrftPublic.Client/Controls/TrackCard.razor.cs delete mode 100644 DeepDrftPublic.Client/Controls/TrackCard.razor.css delete mode 100644 DeepDrftPublic.Client/Controls/TracksGallery.razor delete mode 100644 DeepDrftPublic.Client/Controls/TracksGallery.razor.cs delete mode 100644 DeepDrftPublic.Client/Controls/TracksGallery.razor.css delete mode 100644 DeepDrftPublic.Client/Pages/AlbumsView.razor.css delete mode 100644 DeepDrftPublic.Client/Pages/TrackDetail.razor delete mode 100644 DeepDrftPublic.Client/Pages/TrackDetail.razor.cs delete mode 100644 DeepDrftPublic.Client/Pages/TracksView.razor delete mode 100644 DeepDrftPublic.Client/Pages/TracksView.razor.cs delete mode 100644 DeepDrftPublic.Client/Pages/TracksView.razor.css delete mode 100644 DeepDrftPublic.Client/ViewModels/TrackDetailViewModel.cs delete mode 100644 DeepDrftPublic.Client/ViewModels/TracksViewModel.cs diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/TrackMetaLabel.razor.css b/DeepDrftPublic.Client/Controls/AudioPlayerBar/TrackMetaLabel.razor.css index 4d4715f..4128169 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/TrackMetaLabel.razor.css +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/TrackMetaLabel.razor.css @@ -1,6 +1,6 @@ /* Single space-between row under the waveform: identity on the left, accents on the right. - Colours come from the MudBlazor theme (the dock surface is theme-aware), so unlike the - always-dark TrackCard glass we do not hard-code green-accent overrides here. */ + Colours come from the MudBlazor theme (the dock surface is theme-aware), so we do not + hard-code green-accent overrides here. */ .track-meta-row { display: flex; align-items: center; diff --git a/DeepDrftPublic.Client/Controls/DeepDrftHero.razor b/DeepDrftPublic.Client/Controls/DeepDrftHero.razor index b4a75d3..9b75c67 100644 --- a/DeepDrftPublic.Client/Controls/DeepDrftHero.razor +++ b/DeepDrftPublic.Client/Controls/DeepDrftHero.razor @@ -6,7 +6,7 @@

- Browse Tracks + Browse Tracks
@code { diff --git a/DeepDrftPublic.Client/Controls/GalleryViewMode.cs b/DeepDrftPublic.Client/Controls/GalleryViewMode.cs deleted file mode 100644 index b1f1cc2..0000000 --- a/DeepDrftPublic.Client/Controls/GalleryViewMode.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DeepDrftPublic.Client.Controls; - -public enum GalleryViewMode -{ - Grid, - List -} diff --git a/DeepDrftPublic.Client/Controls/PlayStateIcon.razor.cs b/DeepDrftPublic.Client/Controls/PlayStateIcon.razor.cs index a9a9ceb..05b1044 100644 --- a/DeepDrftPublic.Client/Controls/PlayStateIcon.razor.cs +++ b/DeepDrftPublic.Client/Controls/PlayStateIcon.razor.cs @@ -38,7 +38,7 @@ public partial class PlayStateIcon : ComponentBase, IDisposable { // The cascade is IsFixed, so the provider's re-render never reaches us; subscribe to the // multicast side-channel to re-render on every player state change. Reference-guarded so - // re-parametering is idempotent. Mirrors AudioPlayerBar / TracksView. + // re-parametering is idempotent. Mirrors AudioPlayerBar. if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService)) { if (_subscribedService != null) diff --git a/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor b/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor index e4ac507..36bb692 100644 --- a/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor +++ b/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor @@ -2,7 +2,7 @@ @* Invariant trio shared by every medium's detail page: a back link, a masthead (title + artist), a play/share affordance row wired to the streaming player, and slots for the medium-specific - hero visual and metadata block. TrackDetail and the Session/Mix detail pages all compose this; + hero visual and metadata block. The Cut/Session/Mix detail pages all compose this; per-medium variance rides the Hero and MetaContent render fragments. *@
diff --git a/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor.cs b/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor.cs index f239838..dce760f 100644 --- a/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor.cs +++ b/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor.cs @@ -7,8 +7,8 @@ namespace DeepDrftPublic.Client.Controls; /// /// Shared detail-page chrome for any release medium: back link, masthead, play/share affordance, /// and hero/meta slots. Owns the play-toggle wiring against the cascaded streaming player so each -/// detail page supplies only its data and medium-specific visuals. Extracted from the original -/// TrackDetail page, which is now a thin consumer of this scaffold. +/// detail page supplies only its data and medium-specific visuals. Each medium's detail page is a +/// thin consumer of this scaffold. /// public partial class ReleaseDetailScaffold : ComponentBase { diff --git a/DeepDrftPublic.Client/Controls/ReleaseGallery.razor b/DeepDrftPublic.Client/Controls/ReleaseGallery.razor index 0dd1ede..7d20bad 100644 --- a/DeepDrftPublic.Client/Controls/ReleaseGallery.razor +++ b/DeepDrftPublic.Client/Controls/ReleaseGallery.razor @@ -1,9 +1,14 @@ @namespace DeepDrftPublic.Client.Controls -@* Card grid of releases that open their own detail page (/{DetailRoute}/{id}). Shared by the - Sessions and Mixes browse pages. Cuts intentionally do not use this — they open the track - gallery filtered by album, a different navigation target. Fully controlled by the parent: - loading and item state are passed in. *@ +@* The single release-card grid for every browse surface (Sessions, Mixes, Cuts, Archive). Cards + open a detail page; how a card computes its href is the only real divergence across surfaces, so + the parent supplies it one of two ways: + - DetailRoute (the simple default): every card links /{DetailRoute}/{id} (Sessions, Mixes). + - HrefResolver (per-card): each card links HrefResolver(release), so Archive routes each card by + its own medium through the one ReleaseRoutes table, and Cuts routes to /cuts/{id}. + HrefResolver wins when both are supplied. The card subtitle defaults to the artist; SubtitleResolver + overrides it (Cuts shows a track count instead). Fully controlled by the parent: loading and item + state are passed in. *@
@@ -33,7 +38,7 @@ { @@ -68,8 +73,27 @@ [Parameter] public required IReadOnlyList Releases { get; set; } [Parameter] public bool Loading { get; set; } - /// Route segment for a card's detail page; a card links to /{DetailRoute}/{id}. - [Parameter] public required string DetailRoute { get; set; } + /// + /// Route segment for a card's detail page; a card links to /{DetailRoute}/{id}. The simple + /// fixed-route default used by Sessions/Mixes. Ignored when is supplied. + /// + [Parameter] public string? DetailRoute { get; set; } + + /// + /// Per-card href resolver. When supplied, a card links to its result instead of the + /// -based href, letting Archive route each card by its own medium and + /// Cuts route to /cuts/{id} (both via ReleaseRoutes.DetailHref). + /// + [Parameter] public Func? HrefResolver { get; set; } + + /// + /// Optional override for a card's subtitle line (defaults to the release artist). Cuts pass a + /// track-count label here. + /// + [Parameter] public Func? SubtitleResolver { get; set; } [Parameter] public string EmptyMessage { get; set; } = "Nothing here yet"; + + private string CardHref(DeepDrftModels.DTOs.ReleaseDto release) + => HrefResolver?.Invoke(release) ?? $"/{DetailRoute}/{release.Id}"; } diff --git a/DeepDrftPublic.Client/Controls/TrackCard.razor b/DeepDrftPublic.Client/Controls/TrackCard.razor deleted file mode 100644 index 750119b..0000000 --- a/DeepDrftPublic.Client/Controls/TrackCard.razor +++ /dev/null @@ -1,213 +0,0 @@ -@{ - var hasLink = !string.IsNullOrEmpty(TrackModel?.EntryKey); - var trackHref = hasLink ? $"/track/{TrackModel!.EntryKey}" : null; - var hasArt = !string.IsNullOrEmpty(TrackModel?.Release?.ImagePath); -} - -@if (ViewMode == GalleryViewMode.Grid) -{ -
- - @* Cover and title/artist link to the detail page; the play button (below, outside any - anchor) stays the sole playback entry point. display:contents keeps the grid intact. *@ - @if (hasLink) - { - - @if (!string.IsNullOrEmpty(TrackModel?.Release?.ImagePath)) - { -
-
- } - else - { -
- } -
- } - else if (!string.IsNullOrEmpty(TrackModel?.Release?.ImagePath)) - { -
-
- } - else - { -
- } - -
- - @if (hasLink) - { - -
- - @TrackModel?.TrackName - - - - @TrackModel?.Release?.Artist - -
-
- } - else - { -
- - @TrackModel?.TrackName - - - - @TrackModel?.Release?.Artist - -
- } - -
- @if (!string.IsNullOrEmpty(TrackModel?.Release?.Title)) - { - - @TrackModel.Release!.Title - - } - - @if (!string.IsNullOrEmpty(TrackModel?.Release?.Genre)) - { - - @TrackModel.Release!.Genre - - } -
- -
- @if (TrackModel?.Release?.ReleaseDate.HasValue == true) - { - - @TrackModel.Release!.ReleaseDate!.Value.Year - - } - else - { -
- } - - -
- -
- -
-} -else -{ -
- - - - @if (hasLink) - { - - @* art thumb *@ - @if (!string.IsNullOrEmpty(TrackModel?.Release?.ImagePath)) - { -
-
- } - else - { -
- } - - @* text block *@ -
- - @TrackModel?.Release?.Artist - - - @TrackModel?.TrackName - -
- - @* right metadata *@ -
- @if (!string.IsNullOrEmpty(TrackModel?.Release?.Genre)) - { - - @TrackModel.Release!.Genre - - } - @if (TrackModel?.Release?.ReleaseDate.HasValue == true) - { - - @TrackModel.Release!.ReleaseDate!.Value.Year - - } -
-
- } - else - { - @* same structure without anchor *@ - @if (!string.IsNullOrEmpty(TrackModel?.Release?.ImagePath)) - { -
-
- } - else - { -
- } -
- - @TrackModel?.Release?.Artist - - - @TrackModel?.TrackName - -
-
- @if (!string.IsNullOrEmpty(TrackModel?.Release?.Genre)) - { - - @TrackModel.Release!.Genre - - } - @if (TrackModel?.Release?.ReleaseDate.HasValue == true) - { - - @TrackModel.Release!.ReleaseDate!.Value.Year - - } -
- } -
-} diff --git a/DeepDrftPublic.Client/Controls/TrackCard.razor.cs b/DeepDrftPublic.Client/Controls/TrackCard.razor.cs deleted file mode 100644 index 3b2dd76..0000000 --- a/DeepDrftPublic.Client/Controls/TrackCard.razor.cs +++ /dev/null @@ -1,34 +0,0 @@ -using DeepDrftModels.DTOs; -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace DeepDrftPublic.Client.Controls; - -public partial class TrackCard : ComponentBase -{ - [Parameter] public required TrackDto TrackModel { get; set; } - [Parameter] public EventCallback OnPlay { get; set; } - [Parameter] public EventCallback OnPause { get; set; } - [Parameter] public bool IsPlaying { get; set; } = false; - [Parameter] public bool IsPaused { get; set; } = false; - [Parameter] public GalleryViewMode ViewMode { get; set; } = GalleryViewMode.Grid; - - // Pause only when actively playing; every other state (idle, paused) reads as "press to play". - private bool IsActivelyPlaying => IsPlaying && !IsPaused; - - private string PlayPauseIcon => - IsActivelyPlaying ? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow; - - private async Task PlayClick() - { - if (IsActivelyPlaying) - { - if (OnPause.HasDelegate) - await OnPause.InvokeAsync(TrackModel); - } - else if (OnPlay.HasDelegate) - { - await OnPlay.InvokeAsync(TrackModel); - } - } -} diff --git a/DeepDrftPublic.Client/Controls/TrackCard.razor.css b/DeepDrftPublic.Client/Controls/TrackCard.razor.css deleted file mode 100644 index 0760684..0000000 --- a/DeepDrftPublic.Client/Controls/TrackCard.razor.css +++ /dev/null @@ -1,197 +0,0 @@ -/* Container — transparent so the absolute-positioned fallback panel or album art - controls the card's background. Glass edge matches NowPlayingCard vocabulary. */ -.deepdrft-track-card-container { - width: 250px; - height: 250px; - min-width: 250px; - position: relative; - overflow: hidden; - background: transparent; - border: 2px solid var(--mud-palette-secondary); -} - -.deepdrft-track-card-bg { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-size: cover; - background-position: center; - filter: brightness(0.7); -} - -.deepdrft-track-card-content { - position: relative; - z-index: 1; - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; - padding: 16px; - background: linear-gradient(to top, - rgba(13, 27, 42, 0.75) 0%, - rgba(13, 27, 42, 0.35) 45%, - rgba(13, 27, 42, 0.00) 100%); -} - -/* Fallback panel — solid navy, opaque so the card reads correctly on both - light and dark page backgrounds. Semi-transparent + blur washes out on white. */ -.deepdrft-track-card-fallback { - position: absolute; - top: 0; left: 0; width: 100%; height: 100%; - background: var(--deepdrft-navy-mid, #162437); - border: 1px solid rgba(250, 250, 248, 0.12); -} - -/* Title: off-white — matches .np-title. - ::deep required: MudText renders its own element, so Blazor isolation - won't stamp b-{hash} on it; ::deep pierces into child component output. */ -::deep .deepdrft-track-title { color: var(--deepdrft-white, #FAFAF8); } - -/* Artist: muted off-white — green reserved for interactive elements (FAB, chip). ::deep for same reason. */ -::deep .deepdrft-track-artist { color: rgba(250, 250, 248, 0.55); } - -/* Meta: muted off-white — matches .np-sub. ::deep for same reason. */ -::deep .deepdrft-track-meta { color: rgba(250, 250, 248, 0.45); } - -/* FAB always green-interactive — card is always dark glass regardless of page theme. - .mud-button-filled-tertiary specificity (0,1,0) in MudBlazor; our (0,1,1) wins. */ -::deep .mud-button-filled-tertiary { - background-color: var(--deepdrft-green-interactive, #3aa163); - color: var(--deepdrft-white, #FAFAF8); -} - -/* Genre chip always green-accent outline/text on the dark glass card. */ -::deep .deepdrft-genre-chip.mud-chip-outlined { - border-color: var(--deepdrft-green-accent, #3D7A68); - color: var(--deepdrft-green-accent, #3D7A68); -} -::deep .deepdrft-genre-chip.mud-chip-color-tertiary { - color: var(--deepdrft-green-accent, #3D7A68); -} - -.deepdrft-track-info-middle { margin: 8px 0; } - -.deepdrft-track-info-bottom { - display: flex; - justify-content: space-between; - align-items: center; -} - -@media (max-width: 480px) { - .deepdrft-track-card-container { - min-width: 200px; - width: 200px; - height: 200px; - } -} - -/* ── Mode A: hover-reveal overlay (art cards only) ──────────────────────── */ - -/* Gate the hidden-at-rest rule on (a) art present and (b) a hover-capable pointer. - Fallback cards (no --art modifier) and touch devices always show the overlay. */ -@media (hover: hover) and (pointer: fine) { - .deepdrft-track-card-container--art .deepdrft-track-card-content { - opacity: 0; - background: transparent; - transition: opacity 180ms ease, background-color 180ms ease; - } - .deepdrft-track-card-container--art:hover .deepdrft-track-card-content { - opacity: 1; - background: rgba(22, 36, 55, 0.82); - transition: opacity 180ms ease, background-color 180ms ease; - } -} - -/* ── Mode B: list row ───────────────────────────────────────────────────── */ - -.deepdrft-track-row { - display: flex; - flex-direction: row; - align-items: center; - gap: 16px; - height: 80px; - padding: 8px 16px; - background: var(--mud-palette-surface); - border: 1px solid var(--mud-palette-divider); - border-radius: 4px; - box-sizing: border-box; - width: 100%; -} - -.deepdrft-track-row-link { - display: flex; - flex-direction: row; - align-items: center; - gap: 16px; - flex: 1 1 auto; - min-width: 0; - text-decoration: none; - color: inherit; -} - -::deep .deepdrft-track-row-fab { - flex: 0 0 auto; -} - -.deepdrft-track-row-thumb { - flex: 0 0 64px; - width: 64px; - height: 64px; - background-size: cover; - background-position: center; - border-radius: 2px; -} - -.deepdrft-track-row-thumb--fallback { - background: var(--deepdrft-navy-mid); - border: 1px solid var(--mud-palette-divider); -} - -.deepdrft-track-row-text { - flex: 1 1 auto; - min-width: 0; - display: flex; - flex-direction: column; - justify-content: center; -} - -.deepdrft-track-row-meta { - flex: 0 0 auto; - display: flex; - flex-direction: column; - align-items: flex-end; - justify-content: center; - gap: 4px; -} - -@media (max-width: 480px) { - .deepdrft-track-row { - height: auto; - min-height: 72px; - padding: 8px 12px; - gap: 10px; - } - .deepdrft-track-row-thumb { - flex: 0 0 48px; - width: 48px; - height: 48px; - } -} - -.deepdrft-track-row--playing { - border-left: 3px solid var(--deepdrft-green-interactive, #3aa163); -} - -/* ── Mode B text: theme-aware overrides (navy on light / off-white on dark) ─ */ - -/* The global ::deep rules above hard-code off-white for the dark glass grid cards. - List rows use --mud-palette-surface as their background, so text must follow - the theme. These selectors have higher specificity (.deepdrft-track-row[b-hash] - vs plain [b-hash]) and win in the cascade. */ -.deepdrft-track-row ::deep .deepdrft-track-title, -.deepdrft-track-row ::deep .deepdrft-track-artist, -.deepdrft-track-row ::deep .deepdrft-track-meta { - color: var(--mud-palette-text-primary); -} diff --git a/DeepDrftPublic.Client/Controls/TracksGallery.razor b/DeepDrftPublic.Client/Controls/TracksGallery.razor deleted file mode 100644 index db42790..0000000 --- a/DeepDrftPublic.Client/Controls/TracksGallery.razor +++ /dev/null @@ -1,36 +0,0 @@ -@if (ViewMode == GalleryViewMode.Grid) -{ - - - @foreach (var track in Tracks) - { - - - - } - - -} -else -{ - -
- @foreach (var track in Tracks) - { - - } -
-
-} diff --git a/DeepDrftPublic.Client/Controls/TracksGallery.razor.cs b/DeepDrftPublic.Client/Controls/TracksGallery.razor.cs deleted file mode 100644 index 47878c7..0000000 --- a/DeepDrftPublic.Client/Controls/TracksGallery.razor.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DeepDrftModels.DTOs; -using Microsoft.AspNetCore.Components; - -namespace DeepDrftPublic.Client.Controls; - -public partial class TracksGallery : ComponentBase -{ - [Parameter] public IEnumerable Tracks { get; set; } = []; - - // Controlled play-state inputs: the parent owns playback truth (the player service) - // and drives these. The gallery is presentational — it only matches by id to decide - // which card reflects the active state. - [Parameter] public TrackDto? ActiveTrack { get; set; } - [Parameter] public bool IsPlaying { get; set; } - [Parameter] public bool IsPaused { get; set; } - [Parameter] public GalleryViewMode ViewMode { get; set; } = GalleryViewMode.Grid; - - [Parameter] public EventCallback OnPlay { get; set; } - [Parameter] public EventCallback OnPause { get; set; } - - private Task HandlePlayClick(TrackDto track) => - OnPlay.HasDelegate ? OnPlay.InvokeAsync(track) : Task.CompletedTask; - - private Task HandlePauseClick(TrackDto track) => - OnPause.HasDelegate ? OnPause.InvokeAsync(track) : Task.CompletedTask; -} diff --git a/DeepDrftPublic.Client/Controls/TracksGallery.razor.css b/DeepDrftPublic.Client/Controls/TracksGallery.razor.css deleted file mode 100644 index 0d0736c..0000000 --- a/DeepDrftPublic.Client/Controls/TracksGallery.razor.css +++ /dev/null @@ -1,15 +0,0 @@ -.tracks-gallery-container { - box-sizing: border-box; -} - -.deepdrft-track-gallery-item-center { - display: flex; - justify-content: center; -} - -.deepdrft-track-list { - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; -} diff --git a/DeepDrftPublic.Client/Layout/Pages.cs b/DeepDrftPublic.Client/Layout/Pages.cs index 90b5abf..54d3320 100644 --- a/DeepDrftPublic.Client/Layout/Pages.cs +++ b/DeepDrftPublic.Client/Layout/Pages.cs @@ -22,8 +22,8 @@ public static class Pages // ARCHIVE (→ the release-cardinal /archive browser) carries the three medium modes as Children. // Above the medium breakpoint the desktop nav flattens them into inline appbar links beside // ARCHIVE (no popover); below the breakpoint the mobile hamburger renders them as an indented - // sub-list under ARCHIVE. /tracks and /genres are intentionally absent from the nav (8.I) — - // their routes (TracksView, GenresView) remain reachable by direct URL. + // sub-list under ARCHIVE. /genres is intentionally absent from the nav (8.I) — its route + // (GenresView) remains reachable by direct URL. public static readonly List MenuPages = [ new() diff --git a/DeepDrftPublic.Client/Pages/AlbumsView.razor b/DeepDrftPublic.Client/Pages/AlbumsView.razor index ff0743b..536d130 100644 --- a/DeepDrftPublic.Client/Pages/AlbumsView.razor +++ b/DeepDrftPublic.Client/Pages/AlbumsView.razor @@ -1,63 +1,12 @@ @page "/cuts" +@using DeepDrftPublic.Client.Controls DeepDrft Cuts -
- - @if (_loading) - { - - @foreach (var _ in Enumerable.Range(0, 8)) - { - -
- -
-
- } -
- } - else if (_albums.Count == 0) - { -
- No albums yet -
- } - else - { - - @foreach (var album in _albums) - { - -
-
- @if (!string.IsNullOrEmpty(album.ImagePath)) - { -
-
- } - else - { -
- } - -
- - @album.Title - - - @album.TrackCount @(album.TrackCount == 1 ? "track" : "tracks") - -
-
-
-
- } -
- } -
-
+@* The shared release-card grid; each card routes to /cuts/{id} via the one ReleaseRoutes table. + Cuts show a track count where other media show the artist, supplied via SubtitleResolver. *@ + diff --git a/DeepDrftPublic.Client/Pages/AlbumsView.razor.cs b/DeepDrftPublic.Client/Pages/AlbumsView.razor.cs index 8933e77..2210cd5 100644 --- a/DeepDrftPublic.Client/Pages/AlbumsView.razor.cs +++ b/DeepDrftPublic.Client/Pages/AlbumsView.razor.cs @@ -19,7 +19,6 @@ public partial class AlbumsView : ComponentBase, IDisposable [Inject] public required IReleaseDataService ReleaseData { get; set; } [Inject] public required PersistentComponentState PersistentState { get; set; } - [Inject] public required NavigationManager Navigation { get; set; } // The medium whose releases this grid shows. Defaults to Cut for the /cuts route; other media // can reuse this component by passing a different value. Drives both the fetch filter and the @@ -34,8 +33,8 @@ public partial class AlbumsView : ComponentBase, IDisposable protected override async Task OnInitializedAsync() { - // Bridge the prerendered fetch across the prerender -> WASM seam (see TracksView). Without - // this, the WASM pass re-fetches and replays the card entrance animations. + // Bridge the prerendered fetch across the prerender -> WASM seam (see MediumBrowseBase). + // Without this, the WASM pass re-fetches and replays the card entrance animations. _persistingSubscription = PersistentState.RegisterOnPersisting(PersistAlbums); if (PersistentState.TryTakeFromJson>(PersistKey, out var restored) && restored is not null) @@ -59,8 +58,9 @@ public partial class AlbumsView : ComponentBase, IDisposable return Task.CompletedTask; } - private void OpenAlbum(ReleaseDto album) - => Navigation.NavigateTo(ReleaseRoutes.DetailHref(album)); + // Cut cards show track count where the shared card otherwise shows the artist. + private static string TrackCountLabel(ReleaseDto album) + => $"{album.TrackCount} {(album.TrackCount == 1 ? "track" : "tracks")}"; public void Dispose() => _persistingSubscription.Dispose(); } diff --git a/DeepDrftPublic.Client/Pages/AlbumsView.razor.css b/DeepDrftPublic.Client/Pages/AlbumsView.razor.css deleted file mode 100644 index bc97c05..0000000 --- a/DeepDrftPublic.Client/Pages/AlbumsView.razor.css +++ /dev/null @@ -1,58 +0,0 @@ -.albums-view-container { - padding-top: 16px; -} - -.album-card-center { - display: flex; - justify-content: center; - width: 100%; -} - -.album-card { - display: flex; - flex-direction: column; - width: 200px; - cursor: pointer; - border-radius: 8px; - overflow: hidden; - transition: transform 120ms ease; -} - -.album-card:hover { - transform: translateY(-4px); -} - -.album-card-cover { - width: 200px; - height: 200px; - background-size: cover; - background-position: center; - background-repeat: no-repeat; -} - -.album-card-cover--fallback { - background-color: var(--mud-palette-dark, #1a2238); -} - -.album-card-body { - padding: 8px 4px 0 4px; - display: flex; - flex-direction: column; - gap: 2px; -} - -/* album-card-title / album-card-count ride on MudText, a child Razor component whose - root Blazor isolation does not scope-stamp; ::deep pierces into its output. */ -::deep .album-card-title { - font-weight: 600; -} - -::deep .album-card-count { - opacity: 0.7; -} - -.albums-empty { - display: flex; - justify-content: center; - padding: 48px 0; -} diff --git a/DeepDrftPublic.Client/Pages/ArchiveView.razor b/DeepDrftPublic.Client/Pages/ArchiveView.razor index 7f67ed4..2a5069e 100644 --- a/DeepDrftPublic.Client/Pages/ArchiveView.razor +++ b/DeepDrftPublic.Client/Pages/ArchiveView.razor @@ -1,12 +1,13 @@ @page "/archive" @using DeepDrftModels.Enums +@using DeepDrftPublic.Client.Controls DeepDrft Archive
@* Search + filter affordances are interactive-only: the debounce timer and chip selection - need WASM. During prerender/non-interactive they are hidden, matching TracksView's gate. + need WASM. During prerender/non-interactive they are hidden behind the interactive gate. The release grid still prerenders so the archive is meaningful before hydration. *@ @if (RendererInfo.IsInteractive) { @@ -59,60 +60,12 @@ }
} - - @if (_loading) - { - - @foreach (var _ in Enumerable.Range(0, 8)) - { - -
- -
-
- } -
- } - else if (_releases.Count == 0) - { -
- No releases found -
- } - else - { - - @foreach (var release in _releases) - { - - - - } - - }
+ + @* The grid itself is the shared release-card component; each card routes by its own medium through + the one ReleaseRoutes table. The Archive-specific search/filter chrome above stays here. *@ +
diff --git a/DeepDrftPublic.Client/Pages/ArchiveView.razor.cs b/DeepDrftPublic.Client/Pages/ArchiveView.razor.cs index 2a3b6ae..ef5b97d 100644 --- a/DeepDrftPublic.Client/Pages/ArchiveView.razor.cs +++ b/DeepDrftPublic.Client/Pages/ArchiveView.razor.cs @@ -9,9 +9,9 @@ namespace DeepDrftPublic.Client.Pages; /// The public archive: a release-cardinal searchable browser over every release across all media /// (Phase 9 §8.H, decision H2). Replaces the former three-card medium overview. Search (Title / /// Artist), an enum-driven medium filter, and a genre filter narrow the release list; each card -/// routes to its per-medium detail. Mirrors the seam: the unfiltered first -/// page is bridged across the prerender -> WASM boundary so hydration neither re-fetches nor replays -/// the card entrance animations. +/// routes to its per-medium detail. Mirrors the seam: the unfiltered +/// first page is bridged across the prerender -> WASM boundary so hydration neither re-fetches nor +/// replays the card entrance animations. /// public partial class ArchiveView : ComponentBase, IDisposable { diff --git a/DeepDrftPublic.Client/Pages/ArchiveView.razor.css b/DeepDrftPublic.Client/Pages/ArchiveView.razor.css index 46473d7..0fa05db 100644 --- a/DeepDrftPublic.Client/Pages/ArchiveView.razor.css +++ b/DeepDrftPublic.Client/Pages/ArchiveView.razor.css @@ -50,63 +50,3 @@ flex: 0 1 auto; } } - -.archive-card-center { - display: flex; - justify-content: center; - width: 100%; -} - -.archive-card-link { - text-decoration: none; - color: inherit; -} - -.archive-release-card { - display: flex; - flex-direction: column; - width: 200px; - cursor: pointer; - border-radius: 8px; - overflow: hidden; - transition: transform 120ms ease; -} - -.archive-release-card:hover { - transform: translateY(-4px); -} - -.archive-release-cover { - width: 200px; - height: 200px; - background-size: cover; - background-position: center; - background-repeat: no-repeat; -} - -.archive-release-cover--fallback { - background-color: var(--mud-palette-dark, #1a2238); -} - -.archive-release-body { - padding: 8px 4px 0 4px; - display: flex; - flex-direction: column; - gap: 2px; -} - -/* archive-release-title / archive-release-artist ride on MudText (child Razor component); ::deep - pierces into its output since Blazor isolation does not scope-stamp child component roots. */ -::deep .archive-release-title { - font-weight: 600; -} - -::deep .archive-release-artist { - opacity: 0.7; -} - -.archive-empty { - display: flex; - justify-content: center; - padding: 48px 0; -} diff --git a/DeepDrftPublic.Client/Pages/Home.razor b/DeepDrftPublic.Client/Pages/Home.razor index c3a3a96..893533e 100644 --- a/DeepDrftPublic.Client/Pages/Home.razor +++ b/DeepDrftPublic.Client/Pages/Home.razor @@ -166,7 +166,7 @@

Immerse yourself. The current is always running.

- Explore the Archive + Explore the Archive @* TODO: route to /schedule when live-session schedule page exists *@
diff --git a/DeepDrftPublic.Client/Pages/MediumBrowseBase.cs b/DeepDrftPublic.Client/Pages/MediumBrowseBase.cs index de7c56e..6641313 100644 --- a/DeepDrftPublic.Client/Pages/MediumBrowseBase.cs +++ b/DeepDrftPublic.Client/Pages/MediumBrowseBase.cs @@ -9,7 +9,7 @@ namespace DeepDrftPublic.Client.Pages; /// Shared fetch + prerender-bridge logic for the medium browse pages (Sessions, Mixes). Subclasses /// supply only the and ; this base fetches the paged /// releases and bridges the prerendered result across the prerender -> WASM seam so the WASM pass -/// does not re-fetch and replay the card animations (see the TracksView seam). +/// does not re-fetch and replay the card animations. /// public abstract class MediumBrowseBase : ComponentBase, IDisposable { diff --git a/DeepDrftPublic.Client/Pages/ReleaseDetailBase.cs b/DeepDrftPublic.Client/Pages/ReleaseDetailBase.cs index 389fabb..4e5d313 100644 --- a/DeepDrftPublic.Client/Pages/ReleaseDetailBase.cs +++ b/DeepDrftPublic.Client/Pages/ReleaseDetailBase.cs @@ -8,7 +8,7 @@ namespace DeepDrftPublic.Client.Pages; /// Shared load + prerender-bridge logic for the single-release detail pages (Session, Mix). /// Subclasses supply only their markup; this base loads the release through /// and bridges the prerendered release across the prerender -> -/// WASM seam so the WASM pass does not re-fetch (see the TracksView seam). The playable track is +/// WASM seam so the WASM pass does not re-fetch (see the MediumBrowseBase seam). The playable track is /// re-resolved on a restore miss only. /// public abstract class ReleaseDetailBase : ComponentBase, IDisposable diff --git a/DeepDrftPublic.Client/Pages/TrackDetail.razor b/DeepDrftPublic.Client/Pages/TrackDetail.razor deleted file mode 100644 index 84f31c3..0000000 --- a/DeepDrftPublic.Client/Pages/TrackDetail.razor +++ /dev/null @@ -1,101 +0,0 @@ -@page "/track/{EntryKey}" -@using DeepDrftPublic.Client.Controls - -@(ViewModel.Track?.TrackName ?? "Track") - DeepDrft - -@if (ViewModel.IsLoading) -{ -
-
- -
-
- - -
-
- - -
-
-} -else if (ViewModel.NotFound) -{ -
-
- Track not found. - - This track may have been moved or removed. - -
- - All tracks - -
-
-
-} -else if (ViewModel.Track is not null) -{ - var track = ViewModel.Track; - var release = track.Release; - var hasMeta = release is not null - && (release.Title is not null || release.Genre is not null || release.ReleaseDate is not null); - - - -
- @if (!string.IsNullOrEmpty(release?.ImagePath)) - { - - } - else - { - - - - } -
-
- - @if (hasMeta) - { - @if (release?.Title is not null) - { -
- Album - @release.Title -
- } - - @if (release?.Genre is not null) - { -
- - @release.Genre - -
- } - - @if (release?.ReleaseDate is not null) - { -
- Released - @release.ReleaseDate.Value.ToString("MMMM yyyy") -
- } - } -
-
-} diff --git a/DeepDrftPublic.Client/Pages/TrackDetail.razor.cs b/DeepDrftPublic.Client/Pages/TrackDetail.razor.cs deleted file mode 100644 index 778f5be..0000000 --- a/DeepDrftPublic.Client/Pages/TrackDetail.razor.cs +++ /dev/null @@ -1,70 +0,0 @@ -using DeepDrftModels.DTOs; -using DeepDrftPublic.Client.ViewModels; -using Microsoft.AspNetCore.Components; - -namespace DeepDrftPublic.Client.Pages; - -public partial class TrackDetail : ComponentBase, IDisposable -{ - private const string PersistKey = "track-detail"; - - [Parameter] public required string EntryKey { get; set; } - [Inject] public required TrackDetailViewModel ViewModel { get; set; } - [Inject] public required PersistentComponentState PersistentState { get; set; } - - private PersistingComponentStateSubscription _persistingSubscription; - - // The entry key the ViewModel currently holds. Tracks param-only navigations - // (e.g. /track/A -> /track/B) which reuse this component instance and fire - // OnParametersSet without re-running OnInitialized — without this, the page keeps - // the prior track and Play streams the wrong audio. - private string? _loadedEntryKey; - - protected override void OnInitialized() - // Carry the prerendered track across the prerender -> interactive (WASM) seam. - // Without this, the WASM pass gets a fresh scoped ViewModel, re-renders the - // skeleton, and re-fetches. Mirror the TracksView bridge: persist on the way - // out of prerender, restore on the interactive pass, and only fetch on a miss. - => _persistingSubscription = PersistentState.RegisterOnPersisting(PersistTrack); - - protected override async Task OnParametersSetAsync() - { - // Re-run whenever the route key changes. Component instances are reused across - // same-template navigations, so the load decision must live here, not in - // OnInitialized (which fires once per instance). - if (_loadedEntryKey == EntryKey) return; - - // Capture the key synchronously before any await so that a re-entrant call - // (rapid navigation or a re-render that changes EntryKey while Load is in flight) - // sees the correct guard state. Without this, a second OnParametersSetAsync - // for the same EntryKey would bypass the guard above and start a second Load, - // causing two ViewModel.Load calls to race on the single scoped instance. - _loadedEntryKey = EntryKey; - - // Guard the bridge on the key: a payload for a different track must not seed this - // page (stale-bridge bleed across navigation). - if (PersistentState.TryTakeFromJson(PersistKey, out var restored) - && restored is not null - && restored.EntryKey == EntryKey) - { - ViewModel.Track = restored; - ViewModel.NotFound = false; - ViewModel.IsLoading = false; - } - else - { - await ViewModel.Load(EntryKey); - } - } - - private Task PersistTrack() - { - if (ViewModel.Track is not null) - { - PersistentState.PersistAsJson(PersistKey, ViewModel.Track); - } - return Task.CompletedTask; - } - - public void Dispose() => _persistingSubscription.Dispose(); -} diff --git a/DeepDrftPublic.Client/Pages/TracksView.razor b/DeepDrftPublic.Client/Pages/TracksView.razor deleted file mode 100644 index 50825b8..0000000 --- a/DeepDrftPublic.Client/Pages/TracksView.razor +++ /dev/null @@ -1,92 +0,0 @@ -@page "/tracks" -@using DeepDrftPublic.Client.Controls - -DeepDrft Track Gallery - -
-
- @* Search + filter affordances are interactive-only: the debounce timer and pill clear - need WASM. During prerender/non-interactive they are hidden, matching the view-mode - toggle's interactivity gate. *@ - @if (RendererInfo.IsInteractive) - { -
- -
- - @if (ViewModel.FilterAlbum is not null || ViewModel.FilterGenre is not null) - { -
- - @(ViewModel.FilterAlbum is not null - ? $"Album: {ViewModel.FilterAlbum}" - : $"Genre: {ViewModel.FilterGenre}") - -
- } - } - - @if (ViewModel.Page != null) - { -
- - - - - - - - -
-
- -
- - } - else - { -
- - @foreach (var i in Enumerable.Range(0, 12)) - { - - - - } - -
- - } -
-
\ No newline at end of file diff --git a/DeepDrftPublic.Client/Pages/TracksView.razor.cs b/DeepDrftPublic.Client/Pages/TracksView.razor.cs deleted file mode 100644 index bd5d42a..0000000 --- a/DeepDrftPublic.Client/Pages/TracksView.razor.cs +++ /dev/null @@ -1,163 +0,0 @@ -using DeepDrftModels.DTOs; -using DeepDrftPublic.Client.Controls; -using DeepDrftPublic.Client.Services; -using DeepDrftPublic.Client.ViewModels; -using Microsoft.AspNetCore.Components; -using Models.Common; - -namespace DeepDrftPublic.Client.Pages; - -public partial class TracksView : ComponentBase, IDisposable -{ - private const string PersistKey = "tracks-page"; - - [Inject] public required TracksViewModel ViewModel { get; set; } - [Inject] public required PersistentComponentState PersistentState { get; set; } - [Inject] public required NavigationManager Navigation { get; set; } - [CascadingParameter] public required IStreamingPlayerService PlayerService { get; set; } - - // Filter params arrive on the URL: /tracks?album=X, /tracks?genre=Y, /tracks?q=Z. Copied into - // the ViewModel on init before the first fetch so the gallery renders filtered on direct nav. - [SupplyParameterFromQuery(Name = "album")] public string? AlbumQuery { get; set; } - [SupplyParameterFromQuery(Name = "genre")] public string? GenreQuery { get; set; } - [SupplyParameterFromQuery(Name = "q")] public string? SearchQuery { get; set; } - - private IStreamingPlayerService? _subscribedService; - private PersistingComponentStateSubscription _persistingSubscription; - - // Ephemeral view-mode selection — presentation-only, not persisted across navigation. - private GalleryViewMode _viewMode = GalleryViewMode.Grid; - - protected override async Task OnInitializedAsync() - { - // Seed filter state from the URL before any fetch or restore decision. - ViewModel.FilterAlbum = string.IsNullOrWhiteSpace(AlbumQuery) ? null : AlbumQuery; - ViewModel.FilterGenre = string.IsNullOrWhiteSpace(GenreQuery) ? null : GenreQuery; - ViewModel.SearchText = string.IsNullOrWhiteSpace(SearchQuery) ? null : SearchQuery; - - // Carry the prerendered page across the prerender -> interactive (WASM) seam. - // Without this, the WASM pass gets a fresh scoped ViewModel (Page == null), - // re-renders the skeleton, re-fetches, and replaces the gallery DOM a few - // seconds in — replaying TrackCard entrance animations. Mirror the dark-mode - // PersistentComponentState bridge: persist on the way out of prerender, - // restore on the interactive pass, and only fetch on a miss. - _persistingSubscription = PersistentState.RegisterOnPersisting(PersistTracks); - - // The prerendered page is always unfiltered. When the URL carries filter params, that - // restored page is wrong for this view — skip the restore and fetch with the filter. - if (!ViewModel.HasActiveFilter - && PersistentState.TryTakeFromJson>(PersistKey, out var restored) - && restored is not null) - { - ViewModel.Page = restored; - ViewModel.PageNumber = restored.Page; - } - else - { - await SetPage(ViewModel.PageNumber); - } - } - - protected override void OnParametersSet() - { - // The gallery's per-card icons read off the player's live state (CurrentTrack / - // IsPlaying / IsPaused), which mutates outside this component's render path: - // the player bar's play/pause/stop/close all change it directly. The cascade is - // IsFixed, so the provider's re-render never reaches us — subscribe to the - // multicast side-channel and re-render on every state change. - if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService)) - { - if (_subscribedService != null) - _subscribedService.StateChanged -= OnPlayerStateChanged; - - PlayerService.StateChanged += OnPlayerStateChanged; - _subscribedService = PlayerService; - } - } - - private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged); - - private Task PersistTracks() - { - // Only persist the unfiltered page. A filtered page restored onto a later plain /tracks - // visit would show the wrong results, so a filtered render leaves the cache untouched. - if (ViewModel.Page is not null && !ViewModel.HasActiveFilter) - { - PersistentState.PersistAsJson(PersistKey, ViewModel.Page); - } - return Task.CompletedTask; - } - - private async Task SetPage(int newPage) - { - var result = await ViewModel.TrackData.GetPage( - newPage, ViewModel.PageSize, ViewModel.SortBy, ViewModel.IsDescending, - ViewModel.SearchText, ViewModel.FilterAlbum, ViewModel.FilterGenre); - - if (result is { Success: true, Value: PagedResult pageResult }) - { - ViewModel.Page = pageResult; - ViewModel.PageSize = pageResult.PageSize; - } - } - - // Fired by MudTextField after its 400ms DebounceInterval, so only the trailing keystroke in a - // burst reaches here. Resets to page 1 since the result set changes, then re-fetches with the - // active filter (search + any album/genre pill compose). - private async Task OnSearchInput(string? value) - { - ViewModel.SearchText = string.IsNullOrWhiteSpace(value) ? null : value; - ViewModel.PageNumber = 1; - await SetPage(1); - StateHasChanged(); - } - - // Clears the album/genre pill and returns to the unfiltered gallery. Updates the URL (drops the - // query param) and re-fetches in place. SearchText is intentionally left intact — the pill only - // represents FilterAlbum/FilterGenre, not free-text search, so clearing it must not discard an - // active search term. Blazor reuses the component on a same-route query change and does not - // re-run OnInitializedAsync, so the state reset + refetch happen here explicitly rather than - // relying on re-init. - private async Task ClearFilter() - { - ViewModel.FilterAlbum = null; - ViewModel.FilterGenre = null; - ViewModel.PageNumber = 1; - - Navigation.NavigateTo("/tracks"); - await SetPage(1); - StateHasChanged(); - } - - private async Task PlayTrack(TrackDto track) - { - // Resume the current track if it's merely paused; otherwise stream the new selection. - if (PlayerService.CurrentTrack?.Id == track.Id && PlayerService.IsPaused) - { - await PlayerService.TogglePlayPause(); - } - else - { - await PlayerService.SelectTrack(track); - } - } - - private async Task PauseTrack(TrackDto track) - { - if (PlayerService.CurrentTrack?.Id == track.Id && PlayerService.IsPlaying) - { - await PlayerService.TogglePlayPause(); - } - } - - public void Dispose() - { - _persistingSubscription.Dispose(); - - if (_subscribedService != null) - { - _subscribedService.StateChanged -= OnPlayerStateChanged; - _subscribedService = null; - } - } -} diff --git a/DeepDrftPublic.Client/Pages/TracksView.razor.css b/DeepDrftPublic.Client/Pages/TracksView.razor.css deleted file mode 100644 index 1ef4490..0000000 --- a/DeepDrftPublic.Client/Pages/TracksView.razor.css +++ /dev/null @@ -1,45 +0,0 @@ -/* Layout for the tracks page. - Dead flex/height rules removed — the sticky-footer intent they encoded required - a height target that was never set; normal block flow is sufficient for a - paginated gallery. Horizontal inset is owned by MudContainer in TracksGallery. */ - -.tracks-view-container { - padding: 0; -} - -.tracks-content { - padding-top: 16px; -} - -.tracks-footer { - padding: 8px 0; - display: flex; - flex-direction: column; - align-items: center; - gap: 16px; -} - -.tracks-view-header { - display: flex; - justify-content: flex-end; - padding: 0 0 12px 0; -} - -.tracks-search-row { - display: flex; - justify-content: flex-start; - padding: 0 0 12px 0; -} - -/* tracks-search-field rides on MudTextField, whose root is a child Razor component element. - Blazor isolation does not stamp the scope attribute there, so ::deep is required. */ -::deep .tracks-search-field { - max-width: 420px; - width: 100%; -} - -.tracks-filter-pills { - display: flex; - justify-content: flex-start; - padding: 0 0 12px 0; -} diff --git a/DeepDrftPublic.Client/Startup.cs b/DeepDrftPublic.Client/Startup.cs index 18ad2f9..27f5f0b 100644 --- a/DeepDrftPublic.Client/Startup.cs +++ b/DeepDrftPublic.Client/Startup.cs @@ -18,8 +18,6 @@ public static class Startup // prerender — both call DeepDrftAPI over the "DeepDrft.API" client. services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); // Release read surface (Phase 9). Same HTTP posture as the track client — both // WASM and SSR prerender call DeepDrftAPI over the "DeepDrft.API" client. diff --git a/DeepDrftPublic.Client/ViewModels/ReleaseDetailViewModel.cs b/DeepDrftPublic.Client/ViewModels/ReleaseDetailViewModel.cs index ce107a4..4a88569 100644 --- a/DeepDrftPublic.Client/ViewModels/ReleaseDetailViewModel.cs +++ b/DeepDrftPublic.Client/ViewModels/ReleaseDetailViewModel.cs @@ -8,7 +8,7 @@ namespace DeepDrftPublic.Client.ViewModels; /// playable track. The release read surface exposes no track entry directly, so the playable track /// is resolved through the existing track gallery filtered by the release's id (an exact join) — for /// Session/Mix that yields the single track. Scoped; reset every flag per so a -/// reused instance never bleeds across navigations (mirrors TrackDetailViewModel). +/// reused instance never bleeds across navigations. /// public class ReleaseDetailViewModel { diff --git a/DeepDrftPublic.Client/ViewModels/TrackDetailViewModel.cs b/DeepDrftPublic.Client/ViewModels/TrackDetailViewModel.cs deleted file mode 100644 index a6b6aeb..0000000 --- a/DeepDrftPublic.Client/ViewModels/TrackDetailViewModel.cs +++ /dev/null @@ -1,45 +0,0 @@ -using DeepDrftModels.DTOs; -using DeepDrftPublic.Client.Services; - -namespace DeepDrftPublic.Client.ViewModels; - -public class TrackDetailViewModel -{ - public ITrackDataService TrackData { get; } - - public TrackDto? Track { get; set; } - public bool IsLoading { get; set; } = true; - public bool NotFound { get; set; } - - public TrackDetailViewModel(ITrackDataService trackData) - { - TrackData = trackData; - } - - public async Task Load(string entryKey) - { - // Idempotent across navigations: the scoped instance may be reused, so reset - // every flag before the fetch rather than relying on construction defaults. - IsLoading = true; - NotFound = false; - Track = null; - - try - { - var result = await TrackData.GetTrack(entryKey); - - if (result.Success && result.Value is not null) - { - Track = result.Value; - } - else - { - NotFound = true; - } - } - finally - { - IsLoading = false; - } - } -} diff --git a/DeepDrftPublic.Client/ViewModels/TracksViewModel.cs b/DeepDrftPublic.Client/ViewModels/TracksViewModel.cs deleted file mode 100644 index 85507d2..0000000 --- a/DeepDrftPublic.Client/ViewModels/TracksViewModel.cs +++ /dev/null @@ -1,45 +0,0 @@ -using DeepDrftModels.DTOs; -using DeepDrftPublic.Client.Services; -using Models.Common; - -namespace DeepDrftPublic.Client.ViewModels; - -public class TracksViewModel -{ - public ITrackDataService TrackData { get; } - - public int PageNumber { get; set; } = 1; - - public int PageSize - { - get => Page?.PageSize ?? 15; - set - { - if (Page == null) return; - if (value != Page.PageSize) - { - Page.PageSize = value; - } - } - } - public string SortBy { get; set; } = string.Empty; - public bool IsDescending { get; set; } = false; - public PagedResult? Page { get; set; } = null; - - // Active gallery filters. Null/empty means "no filter on this dimension". SearchText is the - // free-text query; FilterAlbum/FilterGenre are exact-match pills driven by the /albums and - // /genres pages via query-string navigation. - public string? SearchText { get; set; } - public string? FilterAlbum { get; set; } - public string? FilterGenre { get; set; } - - public bool HasActiveFilter => - !string.IsNullOrWhiteSpace(SearchText) - || !string.IsNullOrWhiteSpace(FilterAlbum) - || !string.IsNullOrWhiteSpace(FilterGenre); - - public TracksViewModel(ITrackDataService trackData) - { - TrackData = trackData; - } -}