feature: Phase 18.6 Track A — public Settings menu + streaming-quality toggle
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using DeepDrftPublic.Client.Common;
|
||||
using DeepDrftPublic.Client.Services;
|
||||
using DeepDrftPublic.Client.Clients;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@@ -13,6 +14,7 @@ public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
|
||||
[Inject] public required BeaconInterop Beacon { get; set; }
|
||||
[Inject] public required IPlayEventSink PlayEventSink { get; set; }
|
||||
[Inject] public required IAnonIdProvider AnonId { get; set; }
|
||||
[Inject] public required PublicSiteSettings Settings { get; set; }
|
||||
|
||||
private IStreamingPlayerService? _audioPlayerService;
|
||||
private QueueService? _queueService;
|
||||
@@ -26,7 +28,12 @@ public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
|
||||
// EnsureInitializedAsync — that path is correct because audio contexts
|
||||
// require a user gesture anyway. Initializing eagerly here causes 4+
|
||||
// SignalR round-trips before any content is stable.
|
||||
var player = new StreamingAudioPlayerService(AudioInterop, TrackMediaClient, Logger);
|
||||
// Construct the preference-aware player (Phase 18 wave 18.6): it honours the listener's streaming-
|
||||
// quality choice via the ResolveStreamFormatAsync seam while inheriting the 18.5 capability gate and
|
||||
// C2 fallback. PublicSiteSettings is scoped data (already prerender-seeded + WASM-bridged), so passing
|
||||
// it through the constructor is cheap and carries no lifecycle — the telemetry tracker still binds
|
||||
// post-construction below, exactly as before.
|
||||
var player = new PreferenceAwareStreamingPlayerService(AudioInterop, TrackMediaClient, Logger, Settings);
|
||||
|
||||
// Phase 16: bind the play-session tracker to the player after construction, the same way the
|
||||
// queue binds — the player is built with `new`, not DI, so threading telemetry through its
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
@using DeepDrftPublic.Client.Common
|
||||
@using DeepDrftPublic.Client.Controls.Settings
|
||||
|
||||
@*
|
||||
The public-site Settings menu (Phase 18 wave 18.6, §4a). An app-bar trigger opening a MudMenu that renders
|
||||
a settings-item list — NOT a hard-coded control stack. Each entry is a SettingsItem (label + a control
|
||||
fragment bound to a persisted preference), so a future tenant (dark mode) plugs in as a new list entry, not
|
||||
a menu rewire. Today the list holds one item: the streaming-quality toggle.
|
||||
|
||||
The MudMenu items carry OnClick="@(() => {})" + OnTouch so a click inside a control row does not dismiss the
|
||||
menu (MudMenu auto-closes on item activation otherwise), keeping the radio group usable.
|
||||
*@
|
||||
<div class="dd-accent-icon">
|
||||
<MudMenu Icon="@Icons.Material.Filled.Settings"
|
||||
Color="Color.Inherit"
|
||||
AnchorOrigin="Origin.BottomRight"
|
||||
TransformOrigin="Origin.TopRight"
|
||||
AriaLabel="Settings"
|
||||
Class="dd-settings-menu">
|
||||
<div class="dd-settings-panel">
|
||||
<div class="dd-settings-heading">Settings</div>
|
||||
@foreach (var item in _items)
|
||||
{
|
||||
<div class="dd-settings-item">
|
||||
<div class="dd-settings-item-label">@item.Label</div>
|
||||
@item.Control
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</MudMenu>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
// The settings-item list. Built once; adding a preference is appending one SettingsItem with its control
|
||||
// fragment — the menu body above renders whatever is here without knowing what each item is.
|
||||
private readonly List<SettingsItem> _items =
|
||||
[
|
||||
new SettingsItem("Streaming quality", @<StreamQualitySetting />)
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
@using DeepDrftPublic.Client.Common
|
||||
@using DeepDrftPublic.Client.Services
|
||||
|
||||
@*
|
||||
The streaming-quality control (Phase 18 wave 18.6, §4) — the first occupant of the Settings menu. Binds
|
||||
the listener's choice to PublicSiteSettings.StreamQuality and persists it via the cookie seam. Honest
|
||||
capability gate (OQ2 / AC7): on a browser that cannot decode Ogg Opus the Low-data option still selects,
|
||||
but a note tells the listener the effective stream is lossless — we never let the choice silently imply a
|
||||
format that can't play.
|
||||
*@
|
||||
<div class="dd-setting-control">
|
||||
<MudRadioGroup T="StreamQuality" Value="_quality" ValueChanged="OnQualityChanged">
|
||||
<MudRadio T="StreamQuality" Value="StreamQuality.LowData" Color="Color.Primary" Dense="true">
|
||||
Low-data (Opus)
|
||||
</MudRadio>
|
||||
<MudRadio T="StreamQuality" Value="StreamQuality.Lossless" Color="Color.Primary" Dense="true">
|
||||
Lossless (WAV)
|
||||
</MudRadio>
|
||||
</MudRadioGroup>
|
||||
|
||||
@if (_opusUnavailable && _quality == StreamQuality.LowData)
|
||||
{
|
||||
<div class="dd-setting-note">
|
||||
This browser can't decode Opus — you'll stream lossless.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] public required PublicSiteSettings Settings { get; set; }
|
||||
[Inject] public required SettingsCookieService CookieService { get; set; }
|
||||
[Inject] public required AudioInteropService AudioInterop { get; set; }
|
||||
|
||||
private StreamQuality _quality;
|
||||
|
||||
// Null until the capability probe runs (post-render JS interop). false → can decode Opus; true → cannot.
|
||||
private bool _opusUnavailable;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// Read the current preference (already seeded at prerender + bridged into WASM).
|
||||
_quality = Settings.StreamQuality;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
|
||||
// Capability probe is JS interop — only valid once interactive. Surfaces the honest note when the
|
||||
// browser can't decode Ogg Opus, so a Low-data pick reads as "effectively lossless" rather than
|
||||
// silently failing. The player applies the same gate independently; this is purely the UI honesty.
|
||||
var canDecodeOpus = await AudioInterop.CanDecodeOggOpus();
|
||||
if (canDecodeOpus == _opusUnavailable)
|
||||
{
|
||||
_opusUnavailable = !canDecodeOpus;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnQualityChanged(StreamQuality quality)
|
||||
{
|
||||
_quality = quality;
|
||||
await CookieService.SetStreamQualityAsync(quality);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user