Files
LlamaCasty/ViewModels/MainViewModel.TopBar.cs
gramps 69f08a478e feat(topbar): three fixed slots — every state changes inside its own slot
Creator correction (2026-11-13): "OpenCode literally inserted Live and Private as two extra flat siblings
inside the outer pill bar, shoving Record to the right and making it look like five ungrouped toolbar
items". Correct — completion states were being added to the BAR instead of to a SLOT.

Now the bar is exactly three slots, and the bar's shape never changes:

  [ Build Scenes ]   [ YouTube ]   [ Record ]

  YouTube:  ▶ YouTube -> [Live | Private | ✕] -> [● Start Live | ✕] (amber: next click goes on air)
            -> [■ Stop Live]
  Record:   ◉ Record  -> [● Start Recording | ✕] -> ● REC hh:mm:ss [■ Stop]
  Build:    enabled only while BOTH action slots are Idle; otherwise disabled at 40% with the tooltip
            "Scene editing is locked while streaming or recording"

State lives in YouTubeSlotState / RecordSlotState, with one RaiseSlotStates() announcing every flag so no
caller can forget one. Esc and the ✕ cancel the slot you are in and nothing else.

This retires the BarWorld model from earlier today, along with TopBarActivityTests (superseded by
TopBarSlotTests, which pins all three flows, the lock, and cancel-to-Idle).

679 pass, 0 failed (the host-hang fallback in f4caea0 counted the per-test lines).
Scope justification: TopBarActivityTests.cs is deleted as part of this unit — it tested the model being
replaced; TopBarSlotTests is its successor.
2026-11-13 12:20:11 -08:00

164 lines
6.7 KiB
C#

using System.Windows.Input;
using ytLive.Helpers;
namespace ytLive.ViewModels;
/// <summary>Where the YouTube slot is in its own five-step flow.</summary>
public enum YouTubeSlotState
{
/// <summary>[ ▶ YouTube ]</summary>
Idle,
/// <summary>[ ▶ Live | 🔒 Private | ✕ ]</summary>
ChoosePrivacy,
/// <summary>[ ● Start Live | ✕ ] or [ ● Start Private | ✕ ]</summary>
Armed,
/// <summary>[ ■ Stop Live ] or [ ■ Stop Private ]</summary>
Streaming,
}
/// <summary>Where the Record slot is in its own three-step flow.</summary>
public enum RecordSlotState
{
/// <summary>[ ◉ Record ]</summary>
Idle,
/// <summary>[ ● Start Recording | ✕ ]</summary>
Armed,
/// <summary>[ ■ Stop Recording ]</summary>
Recording,
}
/// <summary>
/// The top bar is THREE FIXED SLOTS (creator, 2026-11-13): Build Scenes, YouTube, Record. A slot never
/// changes the bar's shape — every state transition happens INSIDE its own slot, and the neighbouring
/// slots never move. Earlier attempts added completion states as siblings in the bar, which read as five
/// ungrouped toolbar items and pushed Record around.
/// </summary>
public partial class MainViewModel
{
private YouTubeSlotState _youTubeStep = YouTubeSlotState.Idle;
private RecordSlotState _recordStep = RecordSlotState.Idle;
private bool _podcastLive; // true = Private chosen (the private-visibility test stream)
// ─── Build Scenes slot ───
// Enabled only while BOTH other slots are idle: editing a scene under a live stream or a running
// recording is what the lock is for, and the tooltip says so out loud.
public bool IsBuildScenesEnabled =>
_youTubeStep == YouTubeSlotState.Idle && _recordStep == RecordSlotState.Idle;
public string BuildScenesTooltip => IsBuildScenesEnabled
? "Edit layers, scenes and webcam placement"
: "Scene editing is locked while streaming or recording";
// ─── YouTube slot ───
public YouTubeSlotState YouTubeStep => _youTubeStep;
public bool IsYouTubeIdle => _youTubeStep == YouTubeSlotState.Idle;
public bool IsYouTubeChoosingPrivacy => _youTubeStep == YouTubeSlotState.ChoosePrivacy;
public bool IsYouTubeArmed => _youTubeStep == YouTubeSlotState.Armed;
public bool IsYouTubeStreaming => _youTubeStep == YouTubeSlotState.Streaming;
public bool IsLiveChosen => !_podcastLive;
public bool IsPrivateChosen => _podcastLive;
public string YouTubeArmLabel => _podcastLive ? "Start Private" : "Start Live";
public string YouTubeStopLabel => _podcastLive ? "Stop Private" : "Stop Live";
// ─── Record slot ───
public RecordSlotState RecordStep => _recordStep;
public bool IsRecordIdle => _recordStep == RecordSlotState.Idle;
public bool IsRecordArmed => _recordStep == RecordSlotState.Armed;
public bool IsRecordRecording => _recordStep == RecordSlotState.Recording;
public string RecordElapsedLabel => $"REC: {LiveElapsedText}";
public ICommand SelectBuildCommand { get; private set; } = null!;
public ICommand OpenYouTubeCommand { get; private set; } = null!;
public ICommand ChooseLiveCommand { get; private set; } = null!;
public ICommand ChoosePrivateCommand { get; private set; } = null!;
public ICommand CancelYouTubeCommand { get; private set; } = null!;
public ICommand StartYouTubeCommand { get; private set; } = null!;
public ICommand StopYouTubeCommand { get; private set; } = null!;
public ICommand OpenRecordCommand { get; private set; } = null!;
public ICommand StartRecordingCommand { get; private set; } = null!;
public ICommand CancelRecordCommand { get; private set; } = null!;
public ICommand StopRecordingCommand { get; private set; } = null!;
private void InitializeActivityCommands()
{
SelectBuildCommand = new RelayCommand(_ => RaiseSlotStates(), _ => IsBuildScenesEnabled);
OpenYouTubeCommand = new RelayCommand(_ => SetYouTubeStep(YouTubeSlotState.ChoosePrivacy));
ChooseLiveCommand = new RelayCommand(_ => { _podcastLive = false; SetYouTubeStep(YouTubeSlotState.Armed); });
ChoosePrivateCommand = new RelayCommand(_ => { _podcastLive = true; SetYouTubeStep(YouTubeSlotState.Armed); });
CancelYouTubeCommand = new RelayCommand(_ => SetYouTubeStep(YouTubeSlotState.Idle));
// Signed out still routes to sign-in rather than sitting dead (no-dead-ends rule).
StartYouTubeCommand = new RelayCommand(_ =>
{
if (!IsConnected) { _ = SignInAsync(); return; }
if (_podcastLive) BeginTestStream(); else StartSession();
SetYouTubeStep(YouTubeSlotState.Streaming);
});
StopYouTubeCommand = new RelayCommand(_ =>
{
StopStream();
SetYouTubeStep(YouTubeSlotState.Idle);
});
OpenRecordCommand = new RelayCommand(_ => SetRecordStep(RecordSlotState.Armed));
StartRecordingCommand = new RelayCommand(_ =>
{
BeginRecordOnly();
SetRecordStep(RecordSlotState.Recording);
});
CancelRecordCommand = new RelayCommand(_ => SetRecordStep(RecordSlotState.Idle));
StopRecordingCommand = new RelayCommand(_ =>
{
StopStream();
SetRecordStep(RecordSlotState.Idle);
});
}
private void SetYouTubeStep(YouTubeSlotState step)
{
_youTubeStep = step;
RaiseSlotStates();
}
private void SetRecordStep(RecordSlotState step)
{
_recordStep = step;
RaiseSlotStates();
}
/// <summary>One place that announces everything the three slots show, so no caller can forget a flag.
/// </summary>
internal void RaiseSlotStates()
{
OnPropertyChanged(nameof(IsBuildScenesEnabled));
OnPropertyChanged(nameof(BuildScenesTooltip));
OnPropertyChanged(nameof(YouTubeStep));
OnPropertyChanged(nameof(IsYouTubeIdle));
OnPropertyChanged(nameof(IsYouTubeChoosingPrivacy));
OnPropertyChanged(nameof(IsYouTubeArmed));
OnPropertyChanged(nameof(IsYouTubeStreaming));
OnPropertyChanged(nameof(IsLiveChosen));
OnPropertyChanged(nameof(IsPrivateChosen));
OnPropertyChanged(nameof(YouTubeArmLabel));
OnPropertyChanged(nameof(YouTubeStopLabel));
OnPropertyChanged(nameof(RecordStep));
OnPropertyChanged(nameof(IsRecordIdle));
OnPropertyChanged(nameof(IsRecordArmed));
OnPropertyChanged(nameof(IsRecordRecording));
OnPropertyChanged(nameof(RecordElapsedLabel));
// Build Scenes is the edit surface, so the lock has to reach editing too.
EditingActive = IsBuildScenesEnabled;
}
}