From 0f8647790dc3be382586bfa4c25e629e045d60d3 Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 30 Oct 2026 11:17:11 -0700 Subject: [PATCH] fix(top bar): a signed-out ON-AIR press starts sign-in instead of disabling the button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Models/SessionStartDecision.cs | 18 ++++++++++++++ ViewModels/MainViewModel.Streaming.cs | 29 +++++++++++++++++++---- ytLive.Tests/SessionStartDecisionTests.cs | 8 +++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/Models/SessionStartDecision.cs b/Models/SessionStartDecision.cs index 9498b39..20fabee 100644 --- a/Models/SessionStartDecision.cs +++ b/Models/SessionStartDecision.cs @@ -29,6 +29,24 @@ public static class SessionStartDecision return editing ? SessionStartKind.Test : SessionStartKind.Live; } + /// 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). + 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 { SessionStartKind.Record => "Start Recording", diff --git a/ViewModels/MainViewModel.Streaming.cs b/ViewModels/MainViewModel.Streaming.cs index 4f4b5ae..11a5047 100644 --- a/ViewModels/MainViewModel.Streaming.cs +++ b/ViewModels/MainViewModel.Streaming.cs @@ -188,11 +188,30 @@ public partial class MainViewModel : ViewModelBase public string PrimaryStartButtonLabel => SessionStartDecision.LabelFor(PrimaryStartKind); - /// The button's action follows its label: Test runs the same go-live pipeline with - /// `IsTestStream` set, Go Live runs it for real. - public ICommand PrimaryStartCommand => PrimaryStartKind == SessionStartKind.Test - ? StartTestCommand - : StartStreamCommand; + private ICommand? _primaryStartCommand; + + /// The button's action follows its label — and a signed-out ON-AIR press starts the + /// SIGN-IN flow instead of disabling the control (which is what binding straight to + /// did: its CanExecute needs a connection, so the button the + /// creator was told to press could not be pressed). + 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; + } + } /// The End button shows while recording OR live. public bool ShowEndStreamButton => IsLive || IsRecording; diff --git a/ytLive.Tests/SessionStartDecisionTests.cs b/ytLive.Tests/SessionStartDecisionTests.cs index 2860f46..d77a484 100644 --- a/ytLive.Tests/SessionStartDecisionTests.cs +++ b/ytLive.Tests/SessionStartDecisionTests.cs @@ -26,6 +26,14 @@ public class SessionStartDecisionTests Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(true, false, true)); 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). Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(false, false, false)); }