Skip to content

Commit b64753f

Browse files
committed
Fix browser UI validation regressions
1 parent dc9727b commit b64753f

26 files changed

+296
-58
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ Repo-specific design rules:
310310
- Teleprompter speed styling MUST produce a visible but tasteful letter-spacing or kerning change: slower text opens up slightly and faster text tightens slightly, not a no-op.
311311
- Teleprompter reader word styling MUST mirror TPS/editor inline semantics: explicit inline TPS tags control per-word emphasis and color, while section or block emotion sets card context and must not recolor every reader word.
312312
- Teleprompter underline or highlight treatments that span a phrase or block MUST render as one continuous block-level treatment; separate per-word underlines inside the same phrase are forbidden.
313+
- Teleprompter read-state styling MUST mute phrase-level underline or highlight accents once the emphasized text has been read; bright lingering underline accents on already-read text are forbidden.
313314
- Teleprompter reader text MUST appear on the focal guide immediately when a word or block becomes active; visible post-appearance drift or settling onto the guide is forbidden.
314315
- Teleprompter route styles MUST be present on the first paint; a flash of unstyled or late-styled reader UI during route entry is a regression.
315316
- Teleprompter block transitions MUST stay visually consistent: outgoing cards move upward and incoming cards rise from below in the same direction every time; alternating up/down travel is forbidden, and extra settling, bounce, or intermediate card states are forbidden.

src/PrompterOne.Shared/AppShell/Services/AppBootstrapper.cs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,15 @@ public async Task EnsureReadyAsync(CancellationToken cancellationToken = default
7171
var mediaScene = await _settingsStore.LoadAsync<MediaSceneState>(BrowserAppSettingsKeys.SceneSettings, cancellationToken);
7272
if (mediaScene is not null)
7373
{
74+
var (normalizedMediaScene, mediaSceneChanged) = NormalizeMediaScene(mediaScene);
75+
if (mediaSceneChanged)
76+
{
77+
_logger.LogInformation("Normalizing media scene labels from browser storage.");
78+
await _settingsStore.SaveAsync(BrowserAppSettingsKeys.SceneSettings, normalizedMediaScene, cancellationToken);
79+
}
80+
7481
_logger.LogInformation("Restoring media scene from browser storage.");
75-
_mediaSceneService.ApplyState(mediaScene);
82+
_mediaSceneService.ApplyState(normalizedMediaScene);
7683
}
7784

7885
_initialized = true;
@@ -107,4 +114,67 @@ private static int NormalizeLearnWordsPerMinute(int wordsPerMinute, bool migrate
107114

108115
return wordsPerMinute;
109116
}
117+
118+
private static (MediaSceneState State, bool Changed) NormalizeMediaScene(MediaSceneState state)
119+
{
120+
var changed = false;
121+
var normalizedCameras = state.Cameras
122+
.Select(camera =>
123+
{
124+
var normalizedLabel = MediaDeviceLabelSanitizer.Sanitize(camera.Label);
125+
if (string.Equals(normalizedLabel, camera.Label, StringComparison.Ordinal))
126+
{
127+
return camera;
128+
}
129+
130+
changed = true;
131+
return camera with { Label = normalizedLabel };
132+
})
133+
.ToList();
134+
135+
var normalizedPrimaryMicrophoneLabel = NormalizeOptionalLabel(state.PrimaryMicrophoneLabel, ref changed);
136+
var normalizedAudioInputs = state.AudioBus.Inputs
137+
.Select(input =>
138+
{
139+
var normalizedLabel = MediaDeviceLabelSanitizer.Sanitize(input.Label);
140+
if (string.Equals(normalizedLabel, input.Label, StringComparison.Ordinal))
141+
{
142+
return input;
143+
}
144+
145+
changed = true;
146+
return input with { Label = normalizedLabel };
147+
})
148+
.ToList();
149+
150+
if (!changed)
151+
{
152+
return (state, false);
153+
}
154+
155+
return (
156+
state with
157+
{
158+
Cameras = normalizedCameras,
159+
PrimaryMicrophoneLabel = normalizedPrimaryMicrophoneLabel,
160+
AudioBus = state.AudioBus with { Inputs = normalizedAudioInputs }
161+
},
162+
true);
163+
}
164+
165+
private static string? NormalizeOptionalLabel(string? value, ref bool changed)
166+
{
167+
if (value is null)
168+
{
169+
return null;
170+
}
171+
172+
var normalized = MediaDeviceLabelSanitizer.Sanitize(value);
173+
if (!string.Equals(normalized, value, StringComparison.Ordinal))
174+
{
175+
changed = true;
176+
}
177+
178+
return normalized;
179+
}
110180
}

src/PrompterOne.Shared/GoLive/Components/GoLiveCameraPreviewCard.razor

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@namespace PrompterOne.Shared.Components.GoLive
2+
@using PrompterOne.Shared.Services
23

34
<section id="@UiDomIds.GoLive.PreviewCard"
45
class="gl-preview-section"
@@ -14,7 +15,7 @@
1415
<div class="gl-preview-overlay">
1516
<div class="gl-preview-caption">
1617
<span class="gl-preview-label">@Title</span>
17-
<strong data-testid="@UiTestIds.GoLive.PreviewSourceLabel">@Camera?.Label</strong>
18+
<strong data-testid="@UiTestIds.GoLive.PreviewSourceLabel">@CameraLabel</strong>
1819
</div>
1920
</div>
2021
</OverlayContent>
@@ -53,6 +54,7 @@
5354
[Parameter] public string Title { get; set; } = DefaultTitle;
5455

5556
private bool HasCamera => Camera is not null;
57+
private string CameraLabel => Camera is null ? string.Empty : MediaDeviceLabelSanitizer.Sanitize(Camera.Label);
5658

5759
private string PreviewIndicatorState => ShowLiveDot
5860
? LiveState

src/PrompterOne.Shared/GoLive/Components/GoLiveDestinationSourcePicker.razor

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@namespace PrompterOne.Shared.Components.GoLive
2+
@using PrompterOne.Shared.Services
23

34
<section class="live-destination-sources" data-testid="@UiTestIds.GoLive.ProviderSourcePicker(TargetId)">
45
<div class="live-destination-sources-header">
@@ -26,7 +27,7 @@
2627
class="live-destination-source-chip @(isSelected ? ChipEnabledCssClass : null)"
2728
data-testid="@UiTestIds.GoLive.ProviderSourceToggle(TargetId, source.SourceId)"
2829
@onclick="() => ToggleSource.InvokeAsync(source.SourceId)">
29-
<span class="live-destination-source-name">@source.Label</span>
30+
<span class="live-destination-source-name">@FormatSourceLabel(source)</span>
3031
<span class="live-destination-source-state">@(isSelected ? SelectedLabel : AvailableLabel)</span>
3132
</button>
3233
}
@@ -57,6 +58,7 @@
5758
private int SelectedCount => SelectedSourceIdSet.Count;
5859

5960
private HashSet<string> SelectedSourceIdSet => SelectedSourceIds.ToHashSet(StringComparer.Ordinal);
61+
private static string FormatSourceLabel(SceneCameraSource source) => MediaDeviceLabelSanitizer.Sanitize(source.Label);
6062

6163
private string SourceSummary =>
6264
SelectedCount switch

src/PrompterOne.Shared/GoLive/Components/GoLiveSourcesCard.razor

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@namespace PrompterOne.Shared.Components.GoLive
2+
@using PrompterOne.Shared.Services
23

34
<section class="gl-sources-panel">
45
<div class="gl-sources-header">
@@ -50,7 +51,7 @@
5051

5152
<div class="gl-cam-meta">
5253
<div class="gl-cam-top">
53-
<span class="gl-cam-name">@source.Label</span>
54+
<span class="gl-cam-name">@FormatSourceLabel(source)</span>
5455
<span class="gl-cam-hw">@GetSourceStatus(source)</span>
5556
</div>
5657
</div>
@@ -85,7 +86,7 @@
8586

8687
<div class="gl-mic-card">
8788
<span class="section-label">@MicrophoneTitle</span>
88-
<strong>@MicrophoneName</strong>
89+
<strong>@DisplayMicrophoneName</strong>
8990
<span>@MicrophoneRouteLabel</span>
9091
</div>
9192
</section>
@@ -117,6 +118,8 @@
117118
[Parameter] public string Title { get; set; } = DefaultTitle;
118119
[Parameter] public EventCallback<string> ToggleSource { get; set; }
119120
[Parameter] public IReadOnlyList<GoLiveUtilitySourceViewModel> UtilitySources { get; set; } = [];
121+
private string DisplayMicrophoneName => MediaDeviceLabelSanitizer.Sanitize(MicrophoneName);
122+
private static string FormatSourceLabel(SceneCameraSource source) => MediaDeviceLabelSanitizer.Sanitize(source.Label);
120123

121124
private bool IsActive(SceneCameraSource source) =>
122125
string.Equals(source.SourceId, ActiveSourceId, StringComparison.Ordinal);

src/PrompterOne.Shared/GoLive/Pages/GoLivePage.Session.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using PrompterOne.Core.Models.Media;
33
using PrompterOne.Core.Models.Workspace;
44
using PrompterOne.Shared.GoLive.Models;
5+
using PrompterOne.Shared.Services;
56

67
namespace PrompterOne.Shared.Pages;
78

@@ -21,7 +22,9 @@ public partial class GoLivePage
2122
_ => GoLiveText.Session.SessionIdleLabel
2223
};
2324

24-
private string ActiveSourceLabel => ActiveCamera?.Label ?? GoLiveText.Session.CameraFallbackLabel;
25+
private string ActiveSourceLabel => ActiveCamera is null
26+
? GoLiveText.Session.CameraFallbackLabel
27+
: MediaDeviceLabelSanitizer.Sanitize(ActiveCamera.Label);
2528

2629
private bool CanControlProgram => SelectedCamera is not null;
2730

@@ -58,7 +61,9 @@ public partial class GoLivePage
5861
_ => SessionBadgeIdleCssClass
5962
};
6063

61-
private string SelectedSourceLabel => SelectedCamera?.Label ?? GoLiveText.Session.CameraFallbackLabel;
64+
private string SelectedSourceLabel => SelectedCamera is null
65+
? GoLiveText.Session.CameraFallbackLabel
66+
: MediaDeviceLabelSanitizer.Sanitize(SelectedCamera.Label);
6267

6368
private SceneCameraSource? SelectedCamera => ResolveSessionSource(GoLiveSession.State.SelectedSourceId) ?? ActiveCamera;
6469

src/PrompterOne.Shared/GoLive/Pages/GoLivePage.StudioSurface.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using PrompterOne.Core.Models.Media;
33
using PrompterOne.Shared.Components.GoLive;
44
using PrompterOne.Shared.GoLive.Models;
5+
using PrompterOne.Shared.Services;
56

67
namespace PrompterOne.Shared.Pages;
78

@@ -186,8 +187,8 @@ private IReadOnlyList<GoLiveSceneChipViewModel> BuildSceneChips()
186187
var secondaryCamera = SceneCameras.Count > 1 ? SceneCameras[1] : null;
187188
var scenes = new List<GoLiveSceneChipViewModel>
188189
{
189-
new(GoLiveText.Surface.PrimarySceneId, primaryCamera?.Label ?? GoLiveText.Session.CameraFallbackLabel, GoLiveSceneChipKind.Camera, primaryCamera?.SourceId),
190-
new(GoLiveText.Surface.SecondarySceneId, secondaryCamera?.Label ?? GoLiveText.Surface.InterviewSceneFallback, GoLiveSceneChipKind.Split, secondaryCamera?.SourceId),
190+
new(GoLiveText.Surface.PrimarySceneId, primaryCamera is null ? GoLiveText.Session.CameraFallbackLabel : MediaDeviceLabelSanitizer.Sanitize(primaryCamera.Label), GoLiveSceneChipKind.Camera, primaryCamera?.SourceId),
191+
new(GoLiveText.Surface.SecondarySceneId, secondaryCamera is null ? GoLiveText.Surface.InterviewSceneFallback : MediaDeviceLabelSanitizer.Sanitize(secondaryCamera.Label), GoLiveSceneChipKind.Split, secondaryCamera?.SourceId),
191192
new(GoLiveText.Surface.SceneSlidesId, GoLiveText.Surface.SceneSlidesLabel, GoLiveSceneChipKind.Slides, null),
192193
new(GoLiveText.Surface.PictureInPictureSceneId, GoLiveText.Surface.PictureInPictureSceneLabel, GoLiveSceneChipKind.PictureInPicture, primaryCamera?.SourceId)
193194
};

src/PrompterOne.Shared/GoLive/Pages/GoLivePage.razor.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ public partial class GoLivePage : ComponentBase
4343
?? SceneCameras.FirstOrDefault(camera => camera.Transform.Visible)
4444
?? (SceneCameras.Count > 0 ? SceneCameras[0] : null);
4545

46-
private string PrimaryMicrophoneLabel => MediaSceneService.State.PrimaryMicrophoneLabel ?? GoLiveText.Audio.NoMicrophoneLabel;
46+
private string PrimaryMicrophoneLabel => string.IsNullOrWhiteSpace(MediaSceneService.State.PrimaryMicrophoneLabel)
47+
? GoLiveText.Audio.NoMicrophoneLabel
48+
: MediaDeviceLabelSanitizer.Sanitize(MediaSceneService.State.PrimaryMicrophoneLabel);
4749

4850
private string BackRoute => Shell.GetGoLiveBackRoute();
4951

src/PrompterOne.Shared/Media/Components/CameraPreviewTile.razor

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
@namespace PrompterOne.Shared.Components
22
@implements IAsyncDisposable
33
@inject CameraPreviewInterop CameraPreviewInterop
4+
@using PrompterOne.Shared.Services
45

56
<div class="camera-stage-tile" style="@TileStyle">
67
<video id="@_elementId" class="camera-stage-video" autoplay playsinline muted></video>
7-
<div class="camera-stage-label">@Camera.Label</div>
8+
<div class="camera-stage-label">@CameraLabel</div>
89
</div>
910

1011
@code {
@@ -13,6 +14,7 @@
1314

1415
private string _elementId = $"camera-{Guid.NewGuid():N}";
1516
private string? _attachedDeviceId;
17+
private string CameraLabel => MediaDeviceLabelSanitizer.Sanitize(Camera.Label);
1618

1719
private string TileStyle
1820
{
Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Text.RegularExpressions;
21
using Microsoft.JSInterop;
32
using PrompterOne.Core.Abstractions;
43
using PrompterOne.Core.Models.Media;
@@ -17,7 +16,7 @@ public async Task<IReadOnlyList<MediaDeviceInfo>> GetDevicesAsync(CancellationTo
1716

1817
return devices.Select(device => new MediaDeviceInfo(
1918
device.DeviceId,
20-
SanitizeLabel(device.Label),
19+
MediaDeviceLabelSanitizer.Sanitize(device.Label),
2120
device.Kind switch
2221
{
2322
"videoinput" => MediaDeviceKind.Camera,
@@ -28,19 +27,5 @@ public async Task<IReadOnlyList<MediaDeviceInfo>> GetDevicesAsync(CancellationTo
2827
device.IsDefault)).ToList();
2928
}
3029

31-
private static string SanitizeLabel(string? rawLabel)
32-
{
33-
if (string.IsNullOrWhiteSpace(rawLabel))
34-
{
35-
return string.Empty;
36-
}
37-
38-
var cleaned = VendorIdPattern().Replace(rawLabel, string.Empty).Trim();
39-
return string.IsNullOrWhiteSpace(cleaned) ? string.Empty : cleaned;
40-
}
41-
42-
[GeneratedRegex(@"\s*\([0-9a-fA-F]{4}:[0-9a-fA-F]{4}\)")]
43-
private static partial Regex VendorIdPattern();
44-
4530
private sealed record BrowserMediaDeviceDto(string DeviceId, string? Label, string Kind, bool IsDefault);
4631
}

0 commit comments

Comments
 (0)