Skip to content

Commit dde5699

Browse files
committed
Stabilize shell UI and Go Live browser flows
1 parent 11de948 commit dde5699

33 files changed

+1237
-143
lines changed

src/PrompterOne.Shared/AppShell/Layout/MainLayout.razor

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,17 @@
172172
<div class="app-live-widget-overlay"></div>
173173
<div class="app-live-widget-meta">
174174
<span class="app-live-widget-live-pill">@GoLiveWidgetStateLabel</span>
175-
<span class="app-live-widget-timer">@GoLiveWidgetElapsed</span>
175+
<span class="app-live-widget-timer"
176+
data-testid="@UiTestIds.Header.LiveWidgetTimer">@GoLiveWidgetElapsed</span>
176177
</div>
177178
</div>
178179

179180
<div class="app-live-widget-copy">
180181
<span class="app-live-widget-eyebrow">@GoLiveIndicatorCopy</span>
181-
<strong class="app-live-widget-title">@GoLiveWidgetTitle</strong>
182-
<span class="app-live-widget-detail">@GoLiveWidgetDetail</span>
182+
<strong class="app-live-widget-title"
183+
data-testid="@UiTestIds.Header.LiveWidgetTitle">@GoLiveWidgetTitle</strong>
184+
<span class="app-live-widget-detail"
185+
data-testid="@UiTestIds.Header.LiveWidgetDetail">@GoLiveWidgetDetail</span>
183186
</div>
184187
</button>
185188
}

src/PrompterOne.Shared/AppShell/Layout/MainLayout.razor.cs

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public partial class MainLayout : LayoutComponentBase, IDisposable
1414
{
1515
private const string GoLiveWidgetIdleElapsed = "00:00:00";
1616
private const string RouteChangedLogTemplate = "Route changed to {Location}.";
17+
private static readonly TimeSpan GoLiveWidgetRefreshInterval = TimeSpan.FromSeconds(1);
1718

1819
[Inject] private AppBootstrapper Bootstrapper { get; set; } = null!;
1920
[Inject] private AppShellService Shell { get; set; } = null!;
@@ -25,6 +26,9 @@ public partial class MainLayout : LayoutComponentBase, IDisposable
2526
[Inject] private IStringLocalizer<SharedResource> Localizer { get; set; } = null!;
2627
[Inject] private NavigationManager Navigation { get; set; } = null!;
2728

29+
private CancellationTokenSource? _goLiveWidgetRefreshCancellationTokenSource;
30+
private Task? _goLiveWidgetRefreshTask;
31+
2832
private AppShellState ShellState => Shell.State;
2933

3034
private GoLiveSessionState GoLiveSessionState => GoLiveSession.State;
@@ -41,7 +45,7 @@ public partial class MainLayout : LayoutComponentBase, IDisposable
4145

4246
private bool ShowLearnWpmBadge => ShellState.Screen == AppShellScreen.Learn;
4347

44-
private bool ShowReadAction => ShellState.Screen is AppShellScreen.Editor or AppShellScreen.GoLive;
48+
private bool ShowReadAction => ShellState.Screen == AppShellScreen.Editor;
4549

4650
private bool ShowHeaderSubtitle => !string.IsNullOrWhiteSpace(HeaderSubtitle);
4751

@@ -76,9 +80,9 @@ public partial class MainLayout : LayoutComponentBase, IDisposable
7680
? GoLiveSessionState.RecordingStartedAt ?? GoLiveSessionState.StreamStartedAt
7781
: GoLiveSessionState.StreamStartedAt;
7882

79-
private string GoLiveWidgetDetail => string.IsNullOrWhiteSpace(GoLiveSessionState.ScriptSubtitle)
80-
? GoLiveSessionState.PrimaryMicrophoneLabel
81-
: GoLiveSessionState.ScriptSubtitle;
83+
private string GoLiveWidgetDetail => string.IsNullOrWhiteSpace(GoLiveSessionState.PrimaryMicrophoneLabel)
84+
? GoLiveWidgetStateLabel
85+
: GoLiveSessionState.PrimaryMicrophoneLabel;
8286

8387
private string GoLiveWidgetElapsed => FormatSessionElapsed(GoLiveStartedAt);
8488

@@ -90,9 +94,11 @@ public partial class MainLayout : LayoutComponentBase, IDisposable
9094
_ => GoLiveIndicatorCopy
9195
};
9296

93-
private string GoLiveWidgetTitle => string.IsNullOrWhiteSpace(GoLiveSessionState.ActiveSourceLabel)
94-
? GoLiveSessionState.ScriptTitle
95-
: GoLiveSessionState.ActiveSourceLabel;
97+
private string GoLiveWidgetTitle => !string.IsNullOrWhiteSpace(GoLiveSessionState.ActiveSourceLabel)
98+
? GoLiveSessionState.ActiveSourceLabel
99+
: !string.IsNullOrWhiteSpace(GoLiveSessionState.SelectedSourceLabel)
100+
? GoLiveSessionState.SelectedSourceLabel
101+
: Text(UiTextKey.HeaderGoLive);
96102

97103
private bool ShowGoLiveWidget => GoLiveSessionState.HasActiveSession && ShellState.Screen != AppShellScreen.GoLive;
98104

@@ -129,6 +135,7 @@ protected override void OnInitialized()
129135
GoLiveSession.StateChanged += HandleGoLiveSessionChanged;
130136
Shell.TrackNavigation(Navigation.Uri);
131137
SyncShellStateWithCurrentRoute(Navigation.Uri);
138+
UpdateGoLiveWidgetRefreshLoop();
132139
}
133140

134141
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -146,7 +153,11 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
146153

147154
private void HandleShellStateChanged() => InvokeAsync(StateHasChanged);
148155

149-
private void HandleGoLiveSessionChanged() => InvokeAsync(StateHasChanged);
156+
private void HandleGoLiveSessionChanged()
157+
{
158+
UpdateGoLiveWidgetRefreshLoop();
159+
_ = InvokeAsync(StateHasChanged);
160+
}
150161

151162
private void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
152163
{
@@ -228,7 +239,7 @@ private Task HandleHomeClickAsync()
228239

229240
private Task HandleBackClickAsync()
230241
{
231-
Navigation.NavigateTo(AppRoutes.Library);
242+
Navigation.NavigateTo(Shell.GetBackRoute());
232243
return Task.CompletedTask;
233244
}
234245

@@ -263,10 +274,61 @@ private Task HandleLibrarySearchInputAsync(ChangeEventArgs args)
263274
return Task.CompletedTask;
264275
}
265276

277+
private void UpdateGoLiveWidgetRefreshLoop()
278+
{
279+
if (GoLiveSessionState.HasActiveSession)
280+
{
281+
StartGoLiveWidgetRefreshLoop();
282+
return;
283+
}
284+
285+
StopGoLiveWidgetRefreshLoop();
286+
}
287+
288+
private void StartGoLiveWidgetRefreshLoop()
289+
{
290+
if (_goLiveWidgetRefreshTask is not null)
291+
{
292+
return;
293+
}
294+
295+
_goLiveWidgetRefreshCancellationTokenSource = new CancellationTokenSource();
296+
_goLiveWidgetRefreshTask = RefreshGoLiveWidgetAsync(_goLiveWidgetRefreshCancellationTokenSource.Token);
297+
}
298+
299+
private async Task RefreshGoLiveWidgetAsync(CancellationToken cancellationToken)
300+
{
301+
try
302+
{
303+
using var timer = new PeriodicTimer(GoLiveWidgetRefreshInterval);
304+
while (await timer.WaitForNextTickAsync(cancellationToken))
305+
{
306+
await InvokeAsync(StateHasChanged);
307+
}
308+
}
309+
catch (OperationCanceledException)
310+
{
311+
}
312+
}
313+
314+
private void StopGoLiveWidgetRefreshLoop()
315+
{
316+
if (_goLiveWidgetRefreshCancellationTokenSource is null)
317+
{
318+
return;
319+
}
320+
321+
_goLiveWidgetRefreshCancellationTokenSource.Cancel();
322+
_goLiveWidgetRefreshCancellationTokenSource.Dispose();
323+
_goLiveWidgetRefreshCancellationTokenSource = null;
324+
_goLiveWidgetRefreshTask = null;
325+
}
326+
266327
public void Dispose()
267328
{
268329
Navigation.LocationChanged -= HandleLocationChanged;
269330
Shell.StateChanged -= HandleShellStateChanged;
270331
GoLiveSession.StateChanged -= HandleGoLiveSessionChanged;
332+
StopGoLiveWidgetRefreshLoop();
271333
}
272334
}

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

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public sealed class AppShellService
1010

1111
private string _currentRoute = AppRoutes.Library;
1212
private string _goLiveBackRoute = AppRoutes.Library;
13+
private string _settingsBackRoute = AppRoutes.Library;
1314

1415
public event Action? StateChanged;
1516
public event Action<string>? LibrarySearchChanged;
@@ -79,10 +80,23 @@ public void UpdateLibrarySearch(string searchText)
7980

8081
public string GetGoLiveRoute() => BuildScriptScopedRoute(AppShellScreen.GoLive);
8182

83+
public string GetBackRoute() => State.Screen switch
84+
{
85+
AppShellScreen.Learn => GetEditorRoute(),
86+
AppShellScreen.Teleprompter => GetEditorRoute(),
87+
AppShellScreen.Settings => GetSettingsBackRoute(),
88+
AppShellScreen.GoLive => GetGoLiveBackRoute(),
89+
_ => AppRoutes.Library
90+
};
91+
8292
public string GetGoLiveBackRoute() => IsValidGoLiveBackTarget(_goLiveBackRoute)
8393
? _goLiveBackRoute
8494
: AppRoutes.Library;
8595

96+
public string GetSettingsBackRoute() => IsValidSettingsBackTarget(_settingsBackRoute)
97+
? _settingsBackRoute
98+
: AppRoutes.Library;
99+
86100
public void TrackNavigation(string uri)
87101
{
88102
var nextRoute = NormalizeAppRoute(uri);
@@ -91,6 +105,11 @@ public void TrackNavigation(string uri)
91105
return;
92106
}
93107

108+
if (IsSettingsRoute(nextRoute) && IsValidSettingsBackTarget(_currentRoute))
109+
{
110+
_settingsBackRoute = _currentRoute;
111+
}
112+
94113
if (IsGoLiveRoute(nextRoute) && IsValidGoLiveBackTarget(_currentRoute))
95114
{
96115
_goLiveBackRoute = _currentRoute;
@@ -142,14 +161,10 @@ private string BuildScriptScopedRoute(AppShellScreen screen)
142161
}
143162

144163
private static bool IsGoLiveRoute(string route)
145-
{
146-
var querySeparatorIndex = route.IndexOf(QuerySeparator, StringComparison.Ordinal);
147-
var routeBase = querySeparatorIndex >= 0
148-
? route[..querySeparatorIndex]
149-
: route;
164+
=> string.Equals(GetRouteBase(route), AppRoutes.GoLive, StringComparison.Ordinal);
150165

151-
return string.Equals(routeBase, AppRoutes.GoLive, StringComparison.Ordinal);
152-
}
166+
private static bool IsSettingsRoute(string route) =>
167+
string.Equals(GetRouteBase(route), AppRoutes.Settings, StringComparison.Ordinal);
153168

154169
private static bool IsTrackedRoute(string path) => path switch
155170
{
@@ -165,6 +180,17 @@ private static bool IsGoLiveRoute(string route)
165180
private static bool IsValidGoLiveBackTarget(string route) =>
166181
!string.IsNullOrWhiteSpace(route) && !IsGoLiveRoute(route);
167182

183+
private static bool IsValidSettingsBackTarget(string route) =>
184+
!string.IsNullOrWhiteSpace(route) && !IsSettingsRoute(route);
185+
186+
private static string GetRouteBase(string route)
187+
{
188+
var querySeparatorIndex = route.IndexOf(QuerySeparator, StringComparison.Ordinal);
189+
return querySeparatorIndex >= 0
190+
? route[..querySeparatorIndex]
191+
: route;
192+
}
193+
168194
private static string NormalizeAppRoute(string uri)
169195
{
170196
if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri))

src/PrompterOne.Shared/Contracts/UiTestIds.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ public static class Header
1818
public const string LibrarySearch = "library-search";
1919
public const string LibrarySearchSurface = "library-search-surface";
2020
public const string LiveWidget = "header-go-live-widget";
21+
public const string LiveWidgetDetail = "header-go-live-widget-detail";
2122
public const string LiveWidgetPreview = "header-go-live-widget-preview";
23+
public const string LiveWidgetTimer = "header-go-live-widget-timer";
24+
public const string LiveWidgetTitle = "header-go-live-widget-title";
2225
public const string Subtitle = "header-subtitle";
2326
public const string Title = "header-title";
2427
public const string Wpm = "header-wpm";
@@ -147,6 +150,7 @@ public static class Editor
147150
public const string SourceInput = "editor-source-input";
148151
public const string SourceScrollHost = "editor-source-scroll-host";
149152
public const string SourceStage = "editor-source-stage";
153+
public const string Toolbar = "editor-toolbar";
150154
public const string SpeedFast = "editor-speed-fast";
151155
public const string SpeedCustomWpm = "editor-speed-custom-wpm";
152156
public const string SpeedSlow = "editor-speed-slow";
@@ -325,6 +329,8 @@ public static class Settings
325329

326330
public static string AiProviderSave(string providerId) => $"settings-ai-provider-{providerId}-save";
327331

332+
public static string AiProviderSubtitle(string providerId) => $"settings-ai-provider-{providerId}-subtitle";
333+
328334
public static string CameraDevice(string deviceId) => $"settings-camera-device-{deviceId}";
329335

330336
public static string CameraDeviceAction(string deviceId) => $"settings-camera-device-action-{deviceId}";

src/PrompterOne.Shared/Editor/Components/EditorSourcePanel.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
@inject EditorInterop Interop
66

77
<div class="ed-main" data-testid="@UiTestIds.Editor.MainPanel">
8-
<div class="ed-toolbar">
8+
<div class="ed-toolbar" data-testid="@UiTestIds.Editor.Toolbar">
99
@for (var sectionIndex = 0; sectionIndex < ToolbarSections.Count; sectionIndex++)
1010
{
1111
var toolbarSection = ToolbarSections[sectionIndex];

src/PrompterOne.Shared/Editor/Components/EditorSourcePanel.razor.css

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.ed-main{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--glass-bg-light);backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur);border:var(--glass-border);border-radius:var(--radius-xl);box-shadow:var(--glass-shadow),var(--glass-inset);}
2-
.ed-toolbar{display:flex;align-items:stretch;padding:4px 12px;border-bottom:1px solid var(--border-subtle);background:var(--gold-03);flex-shrink:0;gap:0;overflow:visible;scrollbar-width:none;border-radius:var(--radius-xl) var(--radius-xl) 0 0;}
2+
.ed-toolbar{display:flex;align-items:stretch;padding:4px 12px;border-bottom:1px solid var(--border-subtle);background:var(--gold-03);flex-shrink:0;gap:0;overflow-x:auto;overflow-y:visible;scrollbar-width:none;border-radius:var(--radius-xl) var(--radius-xl) 0 0;overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch;}
33
.ed-toolbar::-webkit-scrollbar{display:none;}
4-
.tb-section{display:flex;flex-direction:column;align-items:center;padding:2px 8px;position:relative;}
4+
.tb-section{display:flex;flex:0 0 auto;flex-direction:column;align-items:center;padding:2px 8px;position:relative;}
55
.tb-label{font-size:10px;font-weight:600;letter-spacing:.6px;text-transform:uppercase;color:var(--text-3);margin-top:3px;white-space:nowrap;}
66
.tb-row{display:flex;align-items:center;gap:2px;flex:1;}
77
.tb-div{width:1px;align-self:stretch;background:var(--gold-10);margin:4px 4px;flex-shrink:0;}
@@ -228,6 +228,14 @@ body.theme-light .efb-menu-item[data-tip]::after {
228228

229229
@media (max-width:768px){
230230
.ed-main,.ed-toolbar,.ed-statusbar{border-radius:0;}
231+
.ed-toolbar{padding:4px 10px;}
231232
.ed-source-highlight,.ed-source-input{padding:24px 20px;}
232233
.ed-source-error{margin:0 20px 24px;}
233234
}
235+
236+
@media (max-width:932px), (max-height:430px){
237+
.ed-toolbar{padding:4px 8px;}
238+
.tb-section{padding:2px 6px;}
239+
.tb-label{font-size:9px;letter-spacing:.5px;}
240+
.tb-btn{padding:4px 6px;}
241+
}

src/PrompterOne.Shared/Editor/Pages/EditorPage.Loading.cs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Globalization;
2+
using PrompterOne.Core.Models.Workspace;
3+
using PrompterOne.Shared.Services;
24

35
namespace PrompterOne.Shared.Pages;
46

@@ -44,16 +46,18 @@ private async Task EnsureSessionLoadedAsync()
4446

4547
private async Task LoadScriptFromQueryAsync()
4648
{
47-
var document = await ScriptRepository.GetAsync(ScriptId!);
48-
if (document is not null &&
49-
!string.Equals(SessionService.State.ScriptId, document.Id, StringComparison.Ordinal))
50-
{
51-
await SessionService.OpenAsync(document);
52-
}
49+
var didLoadRequestedScript = await ScriptRouteSessionLoader.EnsureRequestedSessionAsync(
50+
ScriptId,
51+
ScriptRepository,
52+
SessionService);
5353

54-
if (document is not null)
54+
if (didLoadRequestedScript)
5555
{
56-
_createdDate = document.UpdatedAt.LocalDateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
56+
var document = await ScriptRepository.GetAsync(ScriptId!);
57+
if (document is not null)
58+
{
59+
_createdDate = document.UpdatedAt.LocalDateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
60+
}
5761
}
5862
}
5963

@@ -68,6 +72,7 @@ private void PopulateEditorState(bool resetHistory = false)
6872
_draftRevision = checked(_draftRevision + 1);
6973
}
7074

75+
ResetMetadataDefaults(state);
7176
_sourceText = document.Body;
7277
ApplyLoadedMetadata(metadata, state);
7378
_segments = OutlineBuilder.Build(state.ScriptData, document.Body, 0);
@@ -82,6 +87,24 @@ private void PopulateEditorState(bool resetHistory = false)
8287
Shell.ShowEditor(_screenTitle, state.ScriptId);
8388
}
8489

90+
private void ResetMetadataDefaults(ScriptWorkspaceState state)
91+
{
92+
var defaultBaseWpm = state.ScriptData?.TargetWpm ?? 140;
93+
_screenTitle = state.Title;
94+
_author = DefaultAuthor;
95+
_baseWpm = defaultBaseWpm;
96+
_profile = defaultBaseWpm >= 250 ? DefaultProfileRsvp : DefaultProfileActor;
97+
_version = DefaultVersion;
98+
_createdDate = string.IsNullOrWhiteSpace(state.ScriptId)
99+
? DateTime.UtcNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
100+
: _createdDate;
101+
_displayDuration = FormatDuration(state.EstimatedDuration);
102+
_xslowOffset = DefaultXslowOffset;
103+
_slowOffset = DefaultSlowOffset;
104+
_fastOffset = DefaultFastOffset;
105+
_xfastOffset = DefaultXfastOffset;
106+
}
107+
85108
private void UpdateSyntaxDiagnostics()
86109
{
87110
if (string.IsNullOrWhiteSpace(_errorMessage))

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,10 @@ private async Task EnsureSessionLoadedAsync()
131131
{
132132
if (!string.IsNullOrWhiteSpace(ScriptId))
133133
{
134-
var document = await ScriptRepository.GetAsync(ScriptId);
135-
if (document is not null &&
136-
!string.Equals(SessionService.State.ScriptId, document.Id, StringComparison.Ordinal))
137-
{
138-
await SessionService.OpenAsync(document);
139-
}
140-
134+
await ScriptRouteSessionLoader.EnsureRequestedSessionAsync(
135+
ScriptId,
136+
ScriptRepository,
137+
SessionService);
141138
return;
142139
}
143140

0 commit comments

Comments
 (0)