Skip to content

Commit 442021f

Browse files
committed
Show recording block context cues (#41)
1 parent caa51cc commit 442021f

10 files changed

Lines changed: 404 additions & 1 deletion

File tree

src/PrompterOne.Shared/Contracts/UiTestIds.GoLive.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ public static class GoLive
3232
public const string Page = "go-live-page";
3333
public const string ProgramEmpty = "go-live-program-empty";
3434
public const string ProgramCard = "go-live-program-card";
35+
public const string RecordingBlockActive = "go-live-recording-block-active";
36+
public const string RecordingBlockContext = "go-live-recording-block-context";
37+
public const string RecordingBlockContextToggle = "go-live-recording-block-context-toggle";
38+
public const string RecordingBlockNext = "go-live-recording-block-next";
39+
public const string RecordingBlockNextControl = "go-live-recording-block-next-control";
40+
public const string RecordingBlockPrevious = "go-live-recording-block-previous";
41+
public const string RecordingBlockPreviousControl = "go-live-recording-block-previous-control";
3542
public const string ProgramVideo = "go-live-program-video";
3643
public const string PreviewRail = "go-live-preview-rail";
3744
public const string PreviewCard = "go-live-preview-card";

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,43 @@
2222
<OverlayContent>
2323
<div class="gl-program-overlay">
2424
<span class="gl-program-reader-tag">@Text(SettingsStreamingText.SourcePickerSelectedLabel)</span>
25+
@if (ShowRecordingBlockContext && RecordingBlockCues.Count > 0)
26+
{
27+
<div class="gl-recording-block-context"
28+
data-test="@UiTestIds.GoLive.RecordingBlockContext">
29+
<div class="gl-recording-block-context-head">
30+
<span>@Text(GoLiveText.Sidebar.CueLabel) @Text(UiTextKey.CommonBlocks.ToString())</span>
31+
<div class="gl-recording-block-context-controls">
32+
<button type="button"
33+
class="gl-recording-block-nav"
34+
disabled="@(!CanMoveRecordingBlockBackward)"
35+
aria-label="@Text(UiTextKey.TooltipPreviousBlock.ToString())"
36+
data-test="@UiTestIds.GoLive.RecordingBlockPreviousControl"
37+
@onclick="OnPreviousRecordingBlock">
38+
<UiIcon Kind="UiIconKind.ChevronLeft" Size="12" />
39+
</button>
40+
<button type="button"
41+
class="gl-recording-block-nav"
42+
disabled="@(!CanMoveRecordingBlockForward)"
43+
aria-label="@Text(UiTextKey.TooltipNextBlock.ToString())"
44+
data-test="@UiTestIds.GoLive.RecordingBlockNextControl"
45+
@onclick="OnNextRecordingBlock">
46+
<UiIcon Kind="UiIconKind.ChevronRight" Size="12" />
47+
</button>
48+
</div>
49+
</div>
50+
@foreach (var cue in RecordingBlockCues)
51+
{
52+
<article class="gl-recording-block-cue @(cue.IsActive ? "active" : null)"
53+
data-active="@(cue.IsActive ? "true" : "false")"
54+
data-test="@cue.TestId">
55+
<span class="gl-recording-block-role">@cue.RoleLabel</span>
56+
<strong>@cue.Title</strong>
57+
<p>@cue.Text</p>
58+
</article>
59+
}
60+
</div>
61+
}
2562
</div>
2663
</OverlayContent>
2764
<EmptyContent>
@@ -54,11 +91,17 @@
5491

5592
[Parameter] public string ActiveSessionLabel { get; set; } = string.Empty;
5693
[Parameter] public string ActiveSourceLabel { get; set; } = string.Empty;
94+
[Parameter] public bool CanMoveRecordingBlockBackward { get; set; }
95+
[Parameter] public bool CanMoveRecordingBlockForward { get; set; }
5796
[Parameter] public SceneCameraSource? Camera { get; set; }
5897
[Parameter] public bool IsRecordingActive { get; set; }
5998
[Parameter] public bool IsStreamActive { get; set; }
99+
[Parameter] public EventCallback OnNextRecordingBlock { get; set; }
100+
[Parameter] public EventCallback OnPreviousRecordingBlock { get; set; }
60101
[Parameter] public string ProgramResolutionLabel { get; set; } = string.Empty;
102+
[Parameter] public IReadOnlyList<GoLiveRecordingBlockCueViewModel> RecordingBlockCues { get; set; } = [];
61103
[Parameter] public string SelectedSourceLabel { get; set; } = string.Empty;
104+
[Parameter] public bool ShowRecordingBlockContext { get; set; }
62105
[Parameter] public string StageFrameRateLabel { get; set; } = string.Empty;
63106
[Parameter] public string Title { get; set; } = string.Empty;
64107

src/PrompterOne.Shared/GoLive/Components/GoLiveProgramFeedCard.razor.css

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
inset: 0;
6868
display: flex;
6969
align-items: flex-start;
70-
justify-content: flex-start;
70+
justify-content: space-between;
71+
gap: 16px;
7172
padding: 12px;
7273
pointer-events: none;
7374
}
@@ -87,6 +88,101 @@
8788
text-transform: uppercase;
8889
}
8990

91+
.gl-recording-block-context {
92+
align-self: flex-end;
93+
display: flex;
94+
flex-direction: column;
95+
gap: 8px;
96+
width: min(360px, 42%);
97+
max-height: calc(100% - 12px);
98+
padding: 10px;
99+
overflow: hidden;
100+
pointer-events: auto;
101+
border: 1px solid rgba(255, 107, 107, .24);
102+
border-radius: 12px;
103+
background: rgba(5, 5, 8, .74);
104+
box-shadow: 0 16px 40px rgba(0, 0, 0, .28);
105+
}
106+
107+
.gl-recording-block-context-head,
108+
.gl-recording-block-context-controls {
109+
display: flex;
110+
align-items: center;
111+
}
112+
113+
.gl-recording-block-context-head {
114+
justify-content: space-between;
115+
gap: 8px;
116+
color: rgba(255, 245, 224, .72);
117+
font-size: 10px;
118+
font-weight: 800;
119+
letter-spacing: .12em;
120+
text-transform: uppercase;
121+
}
122+
123+
.gl-recording-block-context-controls {
124+
gap: 4px;
125+
}
126+
127+
.gl-recording-block-nav {
128+
display: inline-flex;
129+
align-items: center;
130+
justify-content: center;
131+
width: 24px;
132+
height: 22px;
133+
padding: 0;
134+
border: 1px solid rgba(255, 255, 255, .08);
135+
border-radius: 6px;
136+
background: rgba(255, 255, 255, .06);
137+
color: rgba(255, 245, 224, .78);
138+
}
139+
140+
.gl-recording-block-nav:disabled {
141+
opacity: .35;
142+
}
143+
144+
.gl-recording-block-cue {
145+
display: flex;
146+
flex-direction: column;
147+
gap: 4px;
148+
min-height: 0;
149+
padding: 8px;
150+
border: 1px solid rgba(255, 255, 255, .07);
151+
border-radius: 8px;
152+
background: rgba(255, 255, 255, .045);
153+
color: rgba(248, 241, 226, .56);
154+
}
155+
156+
.gl-recording-block-cue.active {
157+
border-color: rgba(255, 107, 107, .34);
158+
background: rgba(255, 107, 107, .12);
159+
color: rgba(255, 245, 224, .92);
160+
}
161+
162+
.gl-recording-block-role {
163+
color: rgba(255, 245, 224, .44);
164+
font-size: 9px;
165+
font-weight: 800;
166+
letter-spacing: .12em;
167+
text-transform: uppercase;
168+
}
169+
170+
.gl-recording-block-cue strong {
171+
font-size: 12px;
172+
line-height: 1.2;
173+
}
174+
175+
.gl-recording-block-cue p {
176+
display: -webkit-box;
177+
margin: 0;
178+
overflow: hidden;
179+
color: inherit;
180+
font-size: 11px;
181+
line-height: 1.35;
182+
-webkit-box-orient: vertical;
183+
-webkit-line-clamp: 2;
184+
}
185+
90186
.gl-preview-empty {
91187
position: absolute;
92188
inset: 0;
@@ -161,3 +257,23 @@
161257
.gl-program-monitor-shell[data-live-state="streaming"] .gl-monitor-program {
162258
border-color: rgba(16, 185, 129, .26);
163259
}
260+
261+
@media (max-width: 760px) {
262+
.gl-program-overlay {
263+
gap: 8px;
264+
padding: 8px;
265+
}
266+
267+
.gl-recording-block-context {
268+
width: min(280px, 58%);
269+
padding: 8px;
270+
}
271+
272+
.gl-recording-block-cue {
273+
padding: 6px;
274+
}
275+
276+
.gl-recording-block-cue p {
277+
-webkit-line-clamp: 1;
278+
}
279+
}

src/PrompterOne.Shared/GoLive/Components/GoLiveStudioModels.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ public sealed record GoLiveAudioChannelViewModel(
7777
string DetailLabel,
7878
int LevelPercent);
7979

80+
public sealed record GoLiveRecordingBlockCueViewModel(
81+
string TestId,
82+
string RoleLabel,
83+
string Title,
84+
string Text,
85+
bool IsActive);
86+
8087
public sealed record GoLiveRoomParticipantViewModel(
8188
string Id,
8289
string Initial,

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,38 @@ private async Task ToggleSceneOutputAsync(string sourceId)
4747
MediaSceneService.SetIncludeInOutput(sourceId, !camera.Transform.IncludeInOutput);
4848
await PersistSceneAsync();
4949
}
50+
51+
private Task ToggleRecordingBlockContextAsync()
52+
{
53+
_showRecordingBlockContext = !_showRecordingBlockContext;
54+
NormalizeRecordingBlockIndex();
55+
return Task.CompletedTask;
56+
}
57+
58+
private Task MoveRecordingBlockContextAsync(int direction)
59+
{
60+
var blocks = RecordingBlocks;
61+
if (blocks.Count == 0)
62+
{
63+
_recordingBlockIndex = 0;
64+
return Task.CompletedTask;
65+
}
66+
67+
_recordingBlockIndex = Math.Clamp(_recordingBlockIndex + direction, 0, blocks.Count - 1);
68+
return Task.CompletedTask;
69+
}
70+
71+
private int NormalizeRecordingBlockIndex() => NormalizeRecordingBlockIndex(RecordingBlocks.Count);
72+
73+
private int NormalizeRecordingBlockIndex(int blockCount)
74+
{
75+
if (blockCount == 0)
76+
{
77+
_recordingBlockIndex = 0;
78+
return _recordingBlockIndex;
79+
}
80+
81+
_recordingBlockIndex = Math.Clamp(_recordingBlockIndex, 0, blockCount - 1);
82+
return _recordingBlockIndex;
83+
}
5084
}

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
using PrompterOne.Core.Models.Media;
33
using PrompterOne.Core.Models.Streaming;
44
using PrompterOne.Core.Models.Workspace;
5+
using PrompterOne.Core.Services.Preview;
56
using PrompterOne.Shared.Components.GoLive;
67
using PrompterOne.Shared.Contracts;
78
using PrompterOne.Shared.GoLive.Models;
9+
using PrompterOne.Shared.Localization;
810
using PrompterOne.Shared.Services;
911

1012
namespace PrompterOne.Shared.Pages;
@@ -33,8 +35,10 @@ public partial class GoLivePage
3335
private int _customSceneCount;
3436
private bool _fullProgramView;
3537
private bool _muteAllGuests;
38+
private int _recordingBlockIndex;
3639
private bool _roomCreated;
3740
private bool _showLeftRail = true;
41+
private bool _showRecordingBlockContext;
3842
private bool _showRightRail = true;
3943
private bool _talkbackEnabled;
4044

@@ -51,6 +55,10 @@ public partial class GoLivePage
5155

5256
private IReadOnlyList<GoLiveRoomParticipantViewModel> Participants => BuildParticipants();
5357

58+
private IReadOnlyList<GoLiveRecordingBlockCueViewModel> RecordingBlockCues => BuildRecordingBlockCues();
59+
60+
private IReadOnlyList<BlockPreviewModel> RecordingBlocks => BuildRecordingBlocks();
61+
5462
private string RoomCode => BuildRoomCode();
5563

5664
private IReadOnlyList<GoLiveSceneChipViewModel> SceneChips => BuildSceneChips();
@@ -97,8 +105,17 @@ private string GoLiveContentClass
97105

98106
private bool ShowLeftRail => _showLeftRail && !_fullProgramView;
99107

108+
private bool ShowRecordingBlockContext =>
109+
_showRecordingBlockContext
110+
&& GoLiveSession.State.IsRecordingActive
111+
&& RecordingBlocks.Count > 0;
112+
100113
private bool ShowRightRail => _showRightRail && !_fullProgramView;
101114

115+
private bool CanMoveRecordingBlockBackward => NormalizeRecordingBlockIndex() > 0;
116+
117+
private bool CanMoveRecordingBlockForward => NormalizeRecordingBlockIndex() < RecordingBlocks.Count - 1;
118+
102119
private bool CanAddSceneCamera =>
103120
_mediaDevices.Any(device =>
104121
device.Kind == MediaDeviceKind.Camera
@@ -172,6 +189,73 @@ private IReadOnlyList<GoLiveRoomParticipantViewModel> BuildParticipants()
172189
return participants;
173190
}
174191

192+
private IReadOnlyList<BlockPreviewModel> BuildRecordingBlocks()
193+
{
194+
var blocks = new List<BlockPreviewModel>();
195+
foreach (var segment in SessionService.State.PreviewSegments)
196+
{
197+
if (segment.Blocks.Count > 0)
198+
{
199+
blocks.AddRange(segment.Blocks);
200+
continue;
201+
}
202+
203+
if (string.IsNullOrWhiteSpace(segment.Content))
204+
{
205+
continue;
206+
}
207+
208+
blocks.Add(new BlockPreviewModel
209+
{
210+
Title = segment.Title,
211+
TargetWpm = segment.TargetWpm,
212+
Emotion = segment.Emotion,
213+
EmotionKey = segment.EmotionKey,
214+
Text = segment.Content
215+
});
216+
}
217+
218+
return blocks;
219+
}
220+
221+
private IReadOnlyList<GoLiveRecordingBlockCueViewModel> BuildRecordingBlockCues()
222+
{
223+
var blocks = RecordingBlocks;
224+
if (blocks.Count == 0)
225+
{
226+
return [];
227+
}
228+
229+
var activeIndex = NormalizeRecordingBlockIndex(blocks.Count);
230+
var cues = new List<GoLiveRecordingBlockCueViewModel>(3);
231+
AddRecordingBlockCue(cues, blocks, activeIndex - 1, Text(UiTextKey.EditorFindPrevious.ToString()), UiTestIds.GoLive.RecordingBlockPrevious, isActive: false);
232+
AddRecordingBlockCue(cues, blocks, activeIndex, Text(UiTextKey.EditorStructureActiveBlock.ToString()), UiTestIds.GoLive.RecordingBlockActive, isActive: true);
233+
AddRecordingBlockCue(cues, blocks, activeIndex + 1, Text(UiTextKey.EditorFindNext.ToString()), UiTestIds.GoLive.RecordingBlockNext, isActive: false);
234+
return cues;
235+
}
236+
237+
private static void AddRecordingBlockCue(
238+
List<GoLiveRecordingBlockCueViewModel> cues,
239+
IReadOnlyList<BlockPreviewModel> blocks,
240+
int index,
241+
string roleLabel,
242+
string testId,
243+
bool isActive)
244+
{
245+
if (index < 0 || index >= blocks.Count)
246+
{
247+
return;
248+
}
249+
250+
var block = blocks[index];
251+
cues.Add(new GoLiveRecordingBlockCueViewModel(
252+
testId,
253+
roleLabel,
254+
block.Title,
255+
block.Text,
256+
isActive));
257+
}
258+
175259
private string BuildRoomCode()
176260
{
177261
var roomDestination = ResolvePrimaryRoomDestination();

0 commit comments

Comments
 (0)