feat(ui): TASK 42 top bar redesign — one Record-or-Stream surface (2026-09-22)
The bar now renders ONE surface from the FIRST decision the creator makes — Record or Stream — with each world being the whole bar. Creator directive: "I wrote the fucking thing and I still can't figure-out how to do stuff", "forget this one dog plan bullshit". OBS/streaming-tool reference for the one-surface paradigm: https://obsproject.com (single mode toggle + context actions) — cited per spin-guard habit; the go-live/test pattern follows CEV (https://cev-desktop.aSean.xyz) precedent. - Mode: single segmented REC|ON-AIR switch (SegmentToggle/SegmentLabel in Themes/Controls.xaml); radio-exclusive world selection, one tap to flip. - Record world: switch + Start Recording, zero YouTube identity. - Stream world: switch + Go Live + Test + account zone right (Sign In until connected, then avatar with Change Account/Logout context menu). - Test is a child of Stream: procs only stream-armed AND signed-in. - Running: one reality line "dot word elapsed" (REC/LIVE/TEST; green/red/ gold) + End; worlds + switch retire. - Gear moved up from bottom bar, ~3 wordmark letters past the brand, single click opens Settings/Bug/Feature/About menu (TASK 40 Unit B shipped early). - Fix folded in: BeginTestStream now arms OnAirPillOn explicitly (unarmed pump booted with zero encoder outputs -> "At least one output is required" forced-stop cascade from TASK 41's pipeline). - VM: world/reality props + RaiseTopBarModes() wired into StreamStatus, pill, IsRecording, IsTestStream, IsConnected setters. Doc: ai.md top-bar model, ViewModels/index.md, Controls/index.md, TASKS.md, HANDOFF.md, new TASKS/task-42-top-bar-redesign.md; task-40 note. Tests: TopBarModeTests.cs (new Good Dog), PillRadioTests + TestStreamTests gates updated. Build 0 warnings; full suite 314/315 — single abort is the pre-existing env-dependent RealMouseDrag test (Path of Exile 2 running)
This commit is contained in:
@@ -75,6 +75,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(AccountStatusLightToolTip));
|
||||
OnPropertyChanged(nameof(ShowChatInactiveMessage));
|
||||
OnPropertyChanged(nameof(ShowPreviewPlaceholder));
|
||||
RaiseTopBarModes();
|
||||
UpdateLiveVisuals();
|
||||
}
|
||||
}
|
||||
@@ -98,6 +99,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
if (value && _onAirPillOn) OnAirPillOn = false; // record-OR-live — arming REC drops ON-AIR
|
||||
OnPropertyChanged(nameof(ShowPrimaryStartButton));
|
||||
OnPropertyChanged(nameof(CanStartSession));
|
||||
RaiseTopBarModes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +115,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
if (value && _recordPillOn) RecordPillOn = false; // record-OR-live — arming ON-AIR drops REC
|
||||
OnPropertyChanged(nameof(ShowPrimaryStartButton));
|
||||
OnPropertyChanged(nameof(CanStartSession));
|
||||
OnPropertyChanged(nameof(PrimaryStartButtonLabel));
|
||||
RaiseTopBarModes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +137,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(ShowPrimaryStartButton));
|
||||
OnPropertyChanged(nameof(EndButtonText));
|
||||
OnPropertyChanged(nameof(ShowTestButton));
|
||||
RaiseTopBarModes();
|
||||
UpdateSessionTimer();
|
||||
}
|
||||
}
|
||||
@@ -149,15 +152,14 @@ public partial class MainViewModel : ViewModelBase
|
||||
/// <summary>Either output selected makes the button actionable.</summary>
|
||||
public bool CanStartSession => RecordPillOn || OnAirPillOn;
|
||||
|
||||
/// <summary>Face of the primary Start button. The idle face is reality-aware:
|
||||
/// while the ON-AIR pill is armed signed-out it reads "Sign In" so intent-lit
|
||||
/// intent is never a dead-end Start (— the pill is intent, the account is
|
||||
/// reality, and a greyed face would be the dead end that failed to ship, creator
|
||||
/// ruling 2026-09-20-02). The face flips back to "Start" the moment a session
|
||||
/// connects; the armable signed-out pill gets to keep its intent through the
|
||||
/// sign-in.</summary>
|
||||
/// <summary>Face of the Go button names the armed world (top bar redesign): the
|
||||
/// bar renders only ONE world at a time, so the button never has to explain
|
||||
/// itself — it says exactly what pressing it will do. A signed-out ON-AIR Start
|
||||
/// routes to the sign-in inside StartSession (Start — the account step shows up
|
||||
/// beside it as the Sign In button); a signed-out go-live stays the dead-end the
|
||||
/// creator ruling 2026-09-20-02 forbids.</summary>
|
||||
public string PrimaryStartButtonLabel =>
|
||||
IsConnected || !OnAirPillOn ? "Start" : "Sign In";
|
||||
OnAirPillOn ? "Go Live" : "Start Recording";
|
||||
|
||||
/// <summary>The End button shows while recording OR live.</summary>
|
||||
public bool ShowEndStreamButton => IsLive || IsRecording;
|
||||
@@ -170,13 +172,71 @@ public partial class MainViewModel : ViewModelBase
|
||||
: IsTestStream ? "End Test"
|
||||
: "End Stream";
|
||||
|
||||
/// <summary>TASK 41 — the Test button (top bar, left of Start): a one-click
|
||||
/// private test broadcast. Only meaningful signed-in and idle — it disappears
|
||||
/// the moment a session (live OR recording) starts.</summary>
|
||||
public bool ShowTestButton => IsOffline && !IsRecording && IsConnected;
|
||||
/// <summary>TASK 41 — Test is a child of Stream (creator ruling: "Test should only
|
||||
/// proc when we've selected stream"): a dry-run of the livestream, so it exists
|
||||
/// ONLY inside the armed ON-AIR world, signed-in and idle. You can't dry-run a
|
||||
/// recording, so the Record world never offers it.</summary>
|
||||
public bool ShowTestButton => ShowStreamActions && IsConnected;
|
||||
|
||||
public bool CanStartTest => ShowTestButton;
|
||||
|
||||
// ─── Top bar worlds / reality line (top bar redesign) ───
|
||||
//
|
||||
// The bar is ONE surface rendered by the FIRST decision — Record or Stream —
|
||||
// and each world is the whole bar. Idle shows the mode switch + the armed
|
||||
// world's actions; the account zone exists only inside the Stream world. While
|
||||
// running, the switch and the worlds retire and a single reality line
|
||||
// (● word elapsed) + End replace them, so nothing mid-session can be re-armed.
|
||||
|
||||
/// <summary>Idle — the mode switch is present and actionable.</summary>
|
||||
public bool ShowIdleWorld => IsOffline && !IsRecording;
|
||||
|
||||
/// <summary>Running — the reality line (dot + word + elapsed) + End replace the switch.</summary>
|
||||
public bool RunningVisible => IsLive || IsRecording;
|
||||
|
||||
/// <summary>Record world: the switch sits on REC and Go says "Start Recording".
|
||||
/// No account zone, no Test — YouTube doesn't exist here.</summary>
|
||||
public bool ShowRecordAction => ShowIdleWorld && !OnAirPillOn;
|
||||
|
||||
/// <summary>Stream world: the switch sits on ON-AIR; Go says "Go Live" and Test
|
||||
/// appears. The account zone belongs to this world because login is a step in
|
||||
/// THIS sequence.</summary>
|
||||
public bool ShowStreamActions => ShowIdleWorld && OnAirPillOn;
|
||||
|
||||
/// <summary>Sign In appears on the right only inside the Stream world, signed-out
|
||||
/// — the pending step of the stream sequence, made visible exactly when it's next.</summary>
|
||||
public bool ShowSignInButton => ShowStreamActions && !IsConnected;
|
||||
|
||||
/// <summary>The avatar confirms WHICH account is about to stream — only in the
|
||||
/// Stream world, signed-in. Right-click for Change Account / Logout.</summary>
|
||||
public bool ShowAvatarButton => ShowStreamActions && IsConnected;
|
||||
|
||||
/// <summary>The reality word: TEST while a test broadcast runs, LIVE while on the
|
||||
/// air, REC during a local-record session.</summary>
|
||||
public string RunningText => IsTestStream ? "TEST" : IsLive ? "LIVE" : "REC";
|
||||
|
||||
/// <summary>Reality dot color, one meaning per mode: gold = test, red = live,
|
||||
/// green = recording. The pre-spec pulsing lamp never shipped; the word + bar
|
||||
/// color carry the state and the dot just pins it.</summary>
|
||||
public string RunningDotBrush => IsTestStream ? "#9c6f1c" : IsLive ? "#e94560" : "#22c55e";
|
||||
|
||||
/// <summary>Raises every flag the top bar renders, so any state change re-renders
|
||||
/// exactly the world that is now true.</summary>
|
||||
private void RaiseTopBarModes()
|
||||
{
|
||||
OnPropertyChanged(nameof(ShowIdleWorld));
|
||||
OnPropertyChanged(nameof(RunningVisible));
|
||||
OnPropertyChanged(nameof(ShowRecordAction));
|
||||
OnPropertyChanged(nameof(ShowStreamActions));
|
||||
OnPropertyChanged(nameof(ShowSignInButton));
|
||||
OnPropertyChanged(nameof(ShowAvatarButton));
|
||||
OnPropertyChanged(nameof(ShowTestButton));
|
||||
OnPropertyChanged(nameof(CanStartTest));
|
||||
OnPropertyChanged(nameof(RunningText));
|
||||
OnPropertyChanged(nameof(RunningDotBrush));
|
||||
OnPropertyChanged(nameof(PrimaryStartButtonLabel));
|
||||
}
|
||||
|
||||
/// <summary>TASK 41 — the current session is a test broadcast (drives the TEST
|
||||
/// badge + gold top bar + "End Test" face). Internal setter is a test seam like
|
||||
/// IsRecording: <see cref="BeginTestStream"/> flips it before <see cref="StreamStatus"/>
|
||||
@@ -189,6 +249,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
{
|
||||
if (!SetProperty(ref _isTestStream, value)) return;
|
||||
OnPropertyChanged(nameof(EndButtonText));
|
||||
RaiseTopBarModes();
|
||||
UpdateLiveVisuals();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user