fix(top bar): a signed-out ON-AIR press starts sign-in instead of disabling the button
Reported: EDIT on + ON-AIR on, signed out → the button reads "Test Stream" and cannot be pressed. Cause: the primary button bound straight to StartTestCommand, whose CanExecute needs a connection. StartSession already routed signed-out ON-AIR to the sign-in flow; the test path bypassed it. The route is now explicit and pure: RouteFor(kind, connected) → SignIn | Record | Live | Test. The primary command calls SignInAsync for the SignIn route, BeginTestStream for Test, StartSession otherwise — so the label, the action and the sign-in fallback cannot drift apart. Pinned in SessionStartDecisionTests for both live and test, connected and not.
This commit is contained in:
@@ -29,6 +29,24 @@ public static class SessionStartDecision
|
|||||||
return editing ? SessionStartKind.Test : SessionStartKind.Live;
|
return editing ? SessionStartKind.Test : SessionStartKind.Live;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>What pressing the button actually does, given whether YouTube is connected.
|
||||||
|
/// A signed-out ON-AIR press must start the SIGN-IN flow, not sit there disabled — a dead
|
||||||
|
/// primary button is the dead end the design principle forbids (creator, 2026-10-30).</summary>
|
||||||
|
public enum Route
|
||||||
|
{
|
||||||
|
SignIn,
|
||||||
|
Record,
|
||||||
|
Live,
|
||||||
|
Test,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Route RouteFor(SessionStartKind kind, bool connected) => kind switch
|
||||||
|
{
|
||||||
|
SessionStartKind.Record => Route.Record,
|
||||||
|
SessionStartKind.Test => connected ? Route.Test : Route.SignIn,
|
||||||
|
_ => connected ? Route.Live : Route.SignIn,
|
||||||
|
};
|
||||||
|
|
||||||
public static string LabelFor(SessionStartKind kind) => kind switch
|
public static string LabelFor(SessionStartKind kind) => kind switch
|
||||||
{
|
{
|
||||||
SessionStartKind.Record => "Start Recording",
|
SessionStartKind.Record => "Start Recording",
|
||||||
|
|||||||
@@ -188,11 +188,30 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
|
|
||||||
public string PrimaryStartButtonLabel => SessionStartDecision.LabelFor(PrimaryStartKind);
|
public string PrimaryStartButtonLabel => SessionStartDecision.LabelFor(PrimaryStartKind);
|
||||||
|
|
||||||
/// <summary>The button's action follows its label: Test runs the same go-live pipeline with
|
private ICommand? _primaryStartCommand;
|
||||||
/// `IsTestStream` set, Go Live runs it for real.</summary>
|
|
||||||
public ICommand PrimaryStartCommand => PrimaryStartKind == SessionStartKind.Test
|
/// <summary>The button's action follows its label — and a signed-out ON-AIR press starts the
|
||||||
? StartTestCommand
|
/// SIGN-IN flow instead of disabling the control (which is what binding straight to
|
||||||
: StartStreamCommand;
|
/// <see cref="StartTestCommand"/> did: its CanExecute needs a connection, so the button the
|
||||||
|
/// creator was told to press could not be pressed).</summary>
|
||||||
|
public ICommand PrimaryStartCommand =>
|
||||||
|
_primaryStartCommand ??= new RelayCommand(_ => StartFromPrimaryButton(), _ => ShowPrimaryStartButton);
|
||||||
|
|
||||||
|
private void StartFromPrimaryButton()
|
||||||
|
{
|
||||||
|
switch (SessionStartDecision.RouteFor(PrimaryStartKind, IsConnected))
|
||||||
|
{
|
||||||
|
case SessionStartDecision.Route.SignIn:
|
||||||
|
_ = SignInAsync();
|
||||||
|
return;
|
||||||
|
case SessionStartDecision.Route.Test:
|
||||||
|
BeginTestStream();
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
StartSession();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>The End button shows while recording OR live.</summary>
|
/// <summary>The End button shows while recording OR live.</summary>
|
||||||
public bool ShowEndStreamButton => IsLive || IsRecording;
|
public bool ShowEndStreamButton => IsLive || IsRecording;
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ public class SessionStartDecisionTests
|
|||||||
Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(true, false, true));
|
Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(true, false, true));
|
||||||
Assert.Equal("Start Recording", SessionStartDecision.LabelFor(SessionStartKind.Record));
|
Assert.Equal("Start Recording", SessionStartDecision.LabelFor(SessionStartKind.Record));
|
||||||
|
|
||||||
|
// Signed out: an ON-AIR press (live OR test) routes to the sign-in flow, never a dead
|
||||||
|
// button. Recording never needs an account.
|
||||||
|
Assert.Equal(SessionStartDecision.Route.SignIn, SessionStartDecision.RouteFor(SessionStartKind.Live, connected: false));
|
||||||
|
Assert.Equal(SessionStartDecision.Route.SignIn, SessionStartDecision.RouteFor(SessionStartKind.Test, connected: false));
|
||||||
|
Assert.Equal(SessionStartDecision.Route.Live, SessionStartDecision.RouteFor(SessionStartKind.Live, connected: true));
|
||||||
|
Assert.Equal(SessionStartDecision.Route.Test, SessionStartDecision.RouteFor(SessionStartKind.Test, connected: true));
|
||||||
|
Assert.Equal(SessionStartDecision.Route.Record, SessionStartDecision.RouteFor(SessionStartKind.Record, connected: false));
|
||||||
|
|
||||||
// Unarmed start records locally (the no-dead-ends rule).
|
// Unarmed start records locally (the no-dead-ends rule).
|
||||||
Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(false, false, false));
|
Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(false, false, false));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user