Skip to content

Commit 2245f94

Browse files
committed
miror fix for camera
1 parent e9dca79 commit 2245f94

9 files changed

Lines changed: 62 additions & 15 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ Rule format:
114114
- Every runnable test project must declare `MaxParallelTestsForPipeline : EnvironmentAwareParallelLimitBase` with `LocalLimit = 15`; do not keep lower per-project local parallel caps unless the user explicitly asks for an exception.
115115
- Browser-suite runnable test projects must keep `CiLimit = 2` unless the user explicitly approves a different cap; do not inherit a broader default CI worker count after a suite split.
116116
- Local regression verification must include solution-level `dotnet test --solution ./PrompterOne.slnx -m:1` so test-project split changes are proven under the real all-tests entrypoint, not only as isolated per-project runs.
117+
- When the user explicitly asks to validate a test fix in actual GitHub Actions, do not spend more time on local `CI=true` emulation; push the fix and monitor the real CI run instead.
117118
- Selector-contract remediation requests must be handled repo-wide across all relevant test files (`Web.Tests` and `Web.UITests`), not as partial per-file cleanups.
118119
- Repo-wide quality audits and agent-generated review handoff artifacts must be written as root-level task files so other coding agents can pick them up quickly; do not bury those temporary audit results under `docs/` unless the task is explicitly about durable product documentation.
119120
- Repo-wide cleanup and review passes must explicitly inventory forbidden implementation string literals, `MarkupString` or raw-HTML UI composition, duplicated JS/CSS patterns, architecture-boundary drift, and `foreach`-driven test scenarios that should become isolated TUnit cases.
@@ -402,6 +403,7 @@ Repo-specific design rules:
402403
- Teleprompter back navigation MUST stay as visible and readable as the rest of the page controls; a dim or low-contrast back button on the reader screen is a regression.
403404
- Teleprompter MUST expose both horizontal and vertical mirror toggles on the reader screen so tablet or reflected-glass setups can flip the output without leaving the route or editing CSS manually.
404405
- Teleprompter MUST expose an in-reader orientation toggle, matching the phone control pattern, so operators can switch the text flow direction directly on the reader screen without leaving playback.
406+
- Teleprompter reader background video MUST stay transform-synced with the reader surface: horizontal mirror, vertical mirror, and portrait rotation changes applied to the text lane must apply to the camera/video background as well so the composition stays coherent.
405407
- Teleprompter MUST expose a direct in-reader text-size control so operators can enlarge or reduce the live reading text on the teleprompter surface without leaving playback or relying on settings-only defaults.
406408
- Teleprompter desktop chrome MUST expose a real browser fullscreen toggle when the browser supports it; simulating fullscreen with layout-only expansion is not enough.
407409
- Teleprompter desktop progress MUST show segmented read progress by block, similar to Learn progress semantics, so operators can see overall completion and block boundaries at a glance.

src/PrompterOne.Shared/Teleprompter/Pages/TeleprompterPage.ReaderContent.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -510,13 +510,8 @@ private static string ToAlphaColor(string color, double opacity)
510510
return $"rgba({red}, {green}, {blue}, {opacity.ToString("0.##", CultureInfo.InvariantCulture)})";
511511
}
512512

513-
private static string BuildPrimaryCameraStyle(MediaSourceTransform transform)
514-
{
515-
var transformValue = BuildCameraTransform(transform, includeTranslate: false);
516-
return string.IsNullOrWhiteSpace(transformValue)
517-
? string.Empty
518-
: $"transform:{transformValue};";
519-
}
513+
private static string BuildPrimaryCameraTransform(MediaSourceTransform transform) =>
514+
BuildCameraTransform(transform, includeTranslate: false);
520515

521516
private static bool HasSentenceEndingPunctuation(string text) =>
522517
text.IndexOfAny(['.', '!', '?']) >= 0;

src/PrompterOne.Shared/Teleprompter/Pages/TeleprompterPage.ReaderModels.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ private sealed record ReaderCameraLayerViewModel(
5656
string Role,
5757
int Order,
5858
string CssClass,
59-
string Style,
59+
string BaseTransform,
6060
string TestId)
6161
{
6262
public static ReaderCameraLayerViewModel Placeholder { get; } = new(
@@ -66,7 +66,7 @@ private sealed record ReaderCameraLayerViewModel(
6666
Role: "primary",
6767
Order: 0,
6868
CssClass: "rd-camera",
69-
Style: string.Empty,
69+
BaseTransform: string.Empty,
7070
TestId: UiTestIds.Teleprompter.CameraBackground);
7171
}
7272
}

src/PrompterOne.Shared/Teleprompter/Pages/TeleprompterPage.ReaderRendering.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,26 @@ private string BuildBlockIndicatorLabel() =>
9090
private string BuildCameraCssClass() =>
9191
BuildClassList(_cameraLayer.CssClass, _isReaderCameraActive ? ActiveCssClass : null);
9292

93+
private string BuildCameraStyle()
94+
{
95+
var transforms = new List<string>();
96+
var readerTransform = BuildReaderTransform();
97+
98+
if (!string.IsNullOrWhiteSpace(readerTransform))
99+
{
100+
transforms.Add(readerTransform);
101+
}
102+
103+
if (!string.IsNullOrWhiteSpace(_cameraLayer.BaseTransform))
104+
{
105+
transforms.Add(_cameraLayer.BaseTransform);
106+
}
107+
108+
return transforms.Count == 0
109+
? string.Empty
110+
: $"transform-origin:{ReaderMirrorTransformOrigin};transform:{string.Join(' ', transforms)};";
111+
}
112+
93113
private string BuildReaderBackButtonCssClass() =>
94114
BuildClassList(ReaderBackButtonCssClass, _isReaderPlaying ? ReaderReadingActiveCssClass : null);
95115

src/PrompterOne.Shared/Teleprompter/Pages/TeleprompterPage.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<div class="rd">
1414
<TeleprompterReaderBackdrop CameraAutoStart="@_cameraLayer.AutoStart" CameraCssClass="@BuildCameraCssClass()"
1515
CameraDeviceId="@_cameraLayer.DeviceId" CameraElementId="@_cameraLayer.ElementId"
16-
CameraOrder="@_cameraLayer.Order" CameraRole="@_cameraLayer.Role" CameraStyle="@_cameraLayer.Style"
16+
CameraOrder="@_cameraLayer.Order" CameraRole="@_cameraLayer.Role" CameraStyle="@BuildCameraStyle()"
1717
CameraTestId="@_cameraLayer.TestId" CameraTintCssClass="@BuildCameraTintCssClass()"
1818
GradientCssClass="@BuildReaderGradientCssClass()" GradientValue="@_gradientClass"
1919
IsCameraActive="@_isReaderCameraActive" />

src/PrompterOne.Shared/Teleprompter/Pages/TeleprompterPage.razor.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ private async Task PopulateCameraStateAsync()
218218
Role: "primary",
219219
Order: 0,
220220
CssClass: "rd-camera",
221-
Style: BuildPrimaryCameraStyle(concealedTransform),
221+
BaseTransform: BuildPrimaryCameraTransform(concealedTransform),
222222
TestId: UiTestIds.Teleprompter.CameraBackground);
223223
_isReaderCameraActive = false;
224224
_activateReaderCameraAfterRender = false;
@@ -245,7 +245,7 @@ private async Task PopulateCameraStateAsync()
245245
Role: "primary",
246246
Order: 0,
247247
CssClass: "rd-camera",
248-
Style: BuildPrimaryCameraStyle(transform),
248+
BaseTransform: BuildPrimaryCameraTransform(transform),
249249
TestId: UiTestIds.Teleprompter.CameraBackground);
250250
_isReaderCameraActive = autoStart;
251251
_activateReaderCameraAfterRender = _isReaderCameraActive;
@@ -261,7 +261,7 @@ private async Task PopulateCameraStateAsync()
261261
Role: "primary",
262262
Order: 0,
263263
CssClass: "rd-camera",
264-
Style: BuildPrimaryCameraStyle(primarySceneCamera.Transform),
264+
BaseTransform: BuildPrimaryCameraTransform(primarySceneCamera.Transform),
265265
TestId: UiTestIds.Teleprompter.CameraBackground);
266266
_isReaderCameraActive = autoStart;
267267
_activateReaderCameraAfterRender = _isReaderCameraActive;
@@ -280,7 +280,7 @@ private async Task PopulateCameraStateAsync()
280280
Role: "primary",
281281
Order: 0,
282282
CssClass: "rd-camera",
283-
Style: BuildPrimaryCameraStyle(defaultTransform),
283+
BaseTransform: BuildPrimaryCameraTransform(defaultTransform),
284284
TestId: UiTestIds.Teleprompter.CameraBackground);
285285
_isReaderCameraActive = autoStart && !string.IsNullOrWhiteSpace(defaultCamera?.DeviceId);
286286
_activateReaderCameraAfterRender = _isReaderCameraActive;

tests/PrompterOne.Web.Tests/Teleprompter/TeleprompterPersistenceTests.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public void TeleprompterPage_RestoresPersistedReaderLayoutSettings()
7676
$"top:{PersistedFocalPointPercent}%;",
7777
cut.FindByTestId(UiTestIds.Teleprompter.FocalGuide).GetAttribute("style"));
7878
var clusterWrapStyle = cut.FindByTestId(UiTestIds.Teleprompter.ClusterWrap).GetAttribute("style") ?? string.Empty;
79+
var cameraStyle = cut.FindByTestId(UiTestIds.Teleprompter.CameraBackground).GetAttribute("style") ?? string.Empty;
7980
Assert.Equal(
8081
PortraitOrientationValue,
8182
cut.FindByTestId(UiTestIds.Teleprompter.ClusterWrap).GetAttribute("data-reader-orientation"));
@@ -85,6 +86,9 @@ public void TeleprompterPage_RestoresPersistedReaderLayoutSettings()
8586
Assert.Contains(PortraitOrientationTransform, clusterWrapStyle, StringComparison.Ordinal);
8687
Assert.Contains(HorizontalMirrorTransform, clusterWrapStyle, StringComparison.Ordinal);
8788
Assert.Contains(VerticalMirrorTransform, clusterWrapStyle, StringComparison.Ordinal);
89+
Assert.Contains(PortraitOrientationTransform, cameraStyle, StringComparison.Ordinal);
90+
Assert.Contains(HorizontalMirrorTransform, cameraStyle, StringComparison.Ordinal);
91+
Assert.Contains(VerticalMirrorTransform, cameraStyle, StringComparison.Ordinal);
8892
});
8993
}
9094

@@ -124,6 +128,7 @@ public void TeleprompterPage_PersistsReaderLayoutAndCameraPreferenceChanges()
124128
Assert.Equal(BuildWordsPerMinuteLabel(expectedUpdatedSpeedWpm), cut.FindByTestId(UiTestIds.Teleprompter.SpeedValue).TextContent.Trim());
125129
Assert.Equal($"top:{UpdatedFocalPointPercent}%;", cut.FindByTestId(UiTestIds.Teleprompter.FocalGuide).GetAttribute("style"));
126130
var clusterWrapStyle = cut.FindByTestId(UiTestIds.Teleprompter.ClusterWrap).GetAttribute("style") ?? string.Empty;
131+
var cameraStyle = cut.FindByTestId(UiTestIds.Teleprompter.CameraBackground).GetAttribute("style") ?? string.Empty;
127132
Assert.Equal(
128133
PortraitOrientationValue,
129134
cut.FindByTestId(UiTestIds.Teleprompter.ClusterWrap).GetAttribute("data-reader-orientation"));
@@ -133,6 +138,9 @@ public void TeleprompterPage_PersistsReaderLayoutAndCameraPreferenceChanges()
133138
Assert.Contains(PortraitOrientationTransform, clusterWrapStyle, StringComparison.Ordinal);
134139
Assert.Contains(HorizontalMirrorTransform, clusterWrapStyle, StringComparison.Ordinal);
135140
Assert.Contains(VerticalMirrorTransform, clusterWrapStyle, StringComparison.Ordinal);
141+
Assert.Contains(PortraitOrientationTransform, cameraStyle, StringComparison.Ordinal);
142+
Assert.Contains(HorizontalMirrorTransform, cameraStyle, StringComparison.Ordinal);
143+
Assert.Contains(VerticalMirrorTransform, cameraStyle, StringComparison.Ordinal);
136144

137145
Assert.Equal(UpdatedFontScale, savedSettings.FontScale, 2);
138146
Assert.Equal(UpdatedTextWidthRatio, savedSettings.TextWidth, 4);

tests/PrompterOne.Web.UITests.Reader/Teleprompter/TeleprompterMirrorFlowTests.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ private readonly record struct LeftRailLayoutProbe(
1717
double MirrorRight);
1818

1919
[Test]
20-
public Task TeleprompterScreen_ExposesVisibleBackButtonAndMirrorControls() =>
20+
public Task TeleprompterScreen_SyncsMirrorAndOrientationTransformsAcrossTextAndCameraBackground() =>
2121
RunPageAsync(async page =>
2222
{
2323
await page.GotoAsync(BrowserTestConstants.Routes.TeleprompterDemo);
@@ -29,12 +29,16 @@ await Expect(page.GetByTestId(UiTestIds.Teleprompter.Page)).ToBeVisibleAsync(new
2929
var backButton = page.GetByTestId(UiTestIds.Teleprompter.Back);
3030
var mirrorHorizontal = page.GetByTestId(UiTestIds.Teleprompter.MirrorHorizontalToggle);
3131
var mirrorVertical = page.GetByTestId(UiTestIds.Teleprompter.MirrorVerticalToggle);
32+
var orientationToggle = page.GetByTestId(UiTestIds.Teleprompter.OrientationToggle);
3233
var clusterWrap = page.GetByTestId(UiTestIds.Teleprompter.ClusterWrap);
34+
var cameraBackground = page.GetByTestId(UiTestIds.Teleprompter.CameraBackground);
3335

3436
await Expect(backButton).ToBeVisibleAsync();
3537
await Expect(page.GetByTestId(UiTestIds.Teleprompter.MirrorControls)).ToBeVisibleAsync();
3638
await Expect(mirrorHorizontal).ToBeVisibleAsync();
3739
await Expect(mirrorVertical).ToBeVisibleAsync();
40+
await Expect(orientationToggle).ToBeVisibleAsync();
41+
await TeleprompterCameraDriver.EnsureEnabledAsync(page);
3842

3943
var backButtonColor = await GetComputedStyleValueAsync(backButton, BrowserTestConstants.TeleprompterFlow.ColorProperty);
4044
var mirrorButtonColor = await GetComputedStyleValueAsync(mirrorHorizontal, BrowserTestConstants.TeleprompterFlow.ColorProperty);
@@ -50,6 +54,9 @@ await Expect(mirrorHorizontal).ToHaveAttributeAsync(
5054
await Expect(clusterWrap).ToHaveAttributeAsync(
5155
BrowserTestConstants.TeleprompterFlow.StyleAttribute,
5256
new Regex(Regex.Escape(BrowserTestConstants.TeleprompterFlow.MirrorHorizontalTransform), RegexOptions.Compiled));
57+
await Expect(cameraBackground).ToHaveAttributeAsync(
58+
BrowserTestConstants.TeleprompterFlow.StyleAttribute,
59+
new Regex(Regex.Escape(BrowserTestConstants.TeleprompterFlow.MirrorHorizontalTransform), RegexOptions.Compiled));
5360

5461
await mirrorVertical.ClickAsync();
5562
await Expect(mirrorVertical).ToHaveAttributeAsync(
@@ -58,6 +65,20 @@ await Expect(mirrorVertical).ToHaveAttributeAsync(
5865
await Expect(clusterWrap).ToHaveAttributeAsync(
5966
BrowserTestConstants.TeleprompterFlow.StyleAttribute,
6067
new Regex(Regex.Escape(BrowserTestConstants.TeleprompterFlow.MirrorVerticalTransform), RegexOptions.Compiled));
68+
await Expect(cameraBackground).ToHaveAttributeAsync(
69+
BrowserTestConstants.TeleprompterFlow.StyleAttribute,
70+
new Regex(Regex.Escape(BrowserTestConstants.TeleprompterFlow.MirrorVerticalTransform), RegexOptions.Compiled));
71+
72+
await orientationToggle.ClickAsync();
73+
await Expect(clusterWrap).ToHaveAttributeAsync(
74+
BrowserTestConstants.TeleprompterFlow.ReaderOrientationAttribute,
75+
BrowserTestConstants.TeleprompterFlow.OrientationPortraitValue);
76+
await Expect(clusterWrap).ToHaveAttributeAsync(
77+
BrowserTestConstants.TeleprompterFlow.StyleAttribute,
78+
new Regex(Regex.Escape(BrowserTestConstants.TeleprompterFlow.OrientationPortraitTransform), RegexOptions.Compiled));
79+
await Expect(cameraBackground).ToHaveAttributeAsync(
80+
BrowserTestConstants.TeleprompterFlow.StyleAttribute,
81+
new Regex(Regex.Escape(BrowserTestConstants.TeleprompterFlow.OrientationPortraitTransform), RegexOptions.Compiled));
6182

6283
await UiScenarioArtifacts.CapturePageAsync(
6384
page,

tests/PrompterOne.Web.UITests/Support/BrowserTestConstants.ScreenFlows.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ public static class TeleprompterFlow
278278
public const double MaximumMutedPlayButtonBackgroundAlpha = 0.1;
279279
public const int MinimumBalancedTextLineCount = 2;
280280
public const string ReaderOrientationAttribute = "data-reader-orientation";
281+
public const string OrientationPortraitTransform = "rotate(90deg)";
281282
public const string MirrorHorizontalTransform = "scaleX(-1)";
282283
public const string MirrorVerticalTransform = "scaleY(-1)";
283284
public const string OpeningBlock = "Opening Block";

0 commit comments

Comments
 (0)