Build 730 (creator): - Left pill while recording: solid #2ECC71 dot pulsing 1.0 <-> 0.2 on the existing one-second timer, and "REC: hh:mm:ss" in Consolas so the digits cannot jitter the pill's width. Idle face unchanged. - Right pill while recording: a stop square and "Stop Recording"; clicking it runs the normal stop, which halts and resets the clock and returns the pair to "Local MP4" / "Start Recording". - The separate reality line (dot + REC/LIVE + elapsed + End) is now LIVE-ONLY: a recording would otherwise print the same three facts twice. RunningVisible is IsLive, and ShowOutputStage no longer hides itself for a recording (it must be on screen to carry the clock and the stop). Two rules collided and are now separated rather than merged: a creator's Stop on a recording restores the Local MP4 pair (build 730), while a pipeline FAILURE still clears both pills (the 2026-09-01 zombie rule). StopStream(afterFailure:) carries the difference, and the pump-failure path passes it. RealAppHost.Run was rethrowing with `throw failure;`, which destroyed the original stack — a failing assertion inside the action reported the wrapper's line instead of its own. ExceptionDispatchInfo now preserves it; that is how the two failures above were located in one pass instead of five. 680 pass (one audio-pipe instance flake under full-suite load, passes alone), 0 warnings. Scope justification: Assets/starting-background.jpg is outside this unit — the creator's replacement art, still awaiting their visual check.
126 lines
5.9 KiB
C#
126 lines
5.9 KiB
C#
using System.Windows.Input;
|
|
using ytLive.Helpers;
|
|
|
|
namespace ytLive.ViewModels;
|
|
|
|
/// <summary>Which half of the top bar's dual segment is in force.</summary>
|
|
public enum BarWorld
|
|
{
|
|
/// <summary>Build Scenes — the edit surface. The default at launch (creator ruling 2026-11-12).</summary>
|
|
Build,
|
|
|
|
/// <summary>Record / Go Live — the output surface: which output, and the one action that starts it.</summary>
|
|
Output,
|
|
}
|
|
|
|
/// <summary>
|
|
/// The top bar's one decision (creator ruling 2026-11-12): <c>[ Build Scenes ][ Record / Go Live ]</c>.
|
|
/// Build Scenes <b>is</b> edit mode — there is no separate EDIT toggle any more, so "may I edit?" and
|
|
/// "which world am I in?" cannot disagree.
|
|
///
|
|
/// <para>Clicking the highlighted segment is not a no-op: it re-enters that world, which matters while a
|
|
/// session runs because Build Scenes is the only way back to the layers (see
|
|
/// <see cref="LockEditWhileLive"/>).</para>
|
|
/// </summary>
|
|
public partial class MainViewModel
|
|
{
|
|
private BarWorld _barWorld = BarWorld.Build;
|
|
|
|
public BarWorld BarWorld => _barWorld;
|
|
|
|
public bool IsBuildWorld => _barWorld == BarWorld.Build;
|
|
public bool IsOutputWorld => _barWorld == BarWorld.Output;
|
|
|
|
/// <summary>The output stage belongs to the Record / Go Live world, and only while nothing is
|
|
/// running — a live session cannot change its output, so its switch is not offered.</summary>
|
|
// A recording KEEPS the stage visible: the pill carries REC, the elapsed time and the stop. A live
|
|
// stream does not — its reality line takes over (see RunningVisible).
|
|
public bool ShowOutputStage => IsOutputWorld && !IsLive;
|
|
|
|
// ─── The output stage's three states (creator, builds 728/729) ───
|
|
// 1. nothing chosen: [YouTube] [Local MP4]
|
|
// 2. YouTube chosen: [YouTube(red)] [Live Stream] [Private Stream] (Private = the test run)
|
|
// 3. Local MP4 chosen: [Local MP4(red)] [Start Recording] -> [End Recording] while recording
|
|
public bool ShowOutputChoice => ShowOutputStage && !RecordPillOn && !OnAirPillOn;
|
|
public bool ShowYouTubeSelected => ShowOutputStage && OnAirPillOn;
|
|
public bool ShowLocalMp4Selected => ShowOutputStage && RecordPillOn;
|
|
/// <summary>The YouTube chip shows while nothing is chosen and while YouTube IS the choice; once
|
|
/// Local MP4 is chosen the MP4 chip replaces it in the left slot (creator, build 729).</summary>
|
|
public bool YouTubeChipVisible => ShowOutputChoice || ShowYouTubeSelected;
|
|
public bool LocalMp4ChipVisible => ShowOutputChoice || ShowLocalMp4Selected;
|
|
|
|
/// <summary>The left chip reads "Local MP4" while idle and "REC: hh:mm:ss" while recording, so the
|
|
/// tabular figure cannot jitter the pill's width.</summary>
|
|
public string RecordChipText => IsRecording ? $"REC: {LiveElapsedText}" : "Local MP4";
|
|
|
|
public bool ShowStartRecordingAction => ShowLocalMp4Selected && !IsRecording;
|
|
public bool ShowEndRecordingAction => ShowLocalMp4Selected && IsRecording;
|
|
|
|
public ICommand SelectYouTubeCommand { get; private set; } = null!;
|
|
public ICommand SelectLocalMp4Command { get; private set; } = null!;
|
|
public ICommand StartRecordingCommand { get; private set; } = null!;
|
|
public ICommand TestStreamCommand { get; private set; } = null!;
|
|
public ICommand GoLiveCommand { get; private set; } = null!;
|
|
|
|
public ICommand EnterBuildWorldCommand { get; private set; } = null!;
|
|
public ICommand EnterOutputWorldCommand { get; private set; } = null!;
|
|
|
|
private void InitializeWorldCommands()
|
|
{
|
|
SelectYouTubeCommand = new RelayCommand(_ => OnAirPillOn = true, _ => ShowOutputChoice);
|
|
SelectLocalMp4Command = new RelayCommand(_ => RecordPillOn = true, _ => ShowOutputChoice);
|
|
StartRecordingCommand = new RelayCommand(_ => BeginRecordOnly(), _ => ShowStartRecordingAction);
|
|
// A signed-out press starts the sign-in flow instead of sitting dead (no-dead-ends rule).
|
|
TestStreamCommand = new RelayCommand(_ =>
|
|
{
|
|
if (!IsConnected) { _ = SignInAsync(); return; }
|
|
BeginTestStream();
|
|
}, _ => ShowYouTubeSelected);
|
|
GoLiveCommand = new RelayCommand(_ =>
|
|
{
|
|
if (!IsConnected) { _ = SignInAsync(); return; }
|
|
StartSession();
|
|
}, _ => ShowYouTubeSelected);
|
|
|
|
EnterBuildWorldCommand = new RelayCommand(_ => EnterWorld(BarWorld.Build));
|
|
EnterOutputWorldCommand = new RelayCommand(_ => EnterWorld(BarWorld.Output));
|
|
}
|
|
|
|
/// <summary>Everything the output stage shows or hides, in one place so no caller can forget
|
|
/// one of them.</summary>
|
|
internal void RaiseOutputStage()
|
|
{
|
|
OnPropertyChanged(nameof(ShowOutputStage));
|
|
OnPropertyChanged(nameof(ShowOutputChoice));
|
|
OnPropertyChanged(nameof(ShowYouTubeSelected));
|
|
OnPropertyChanged(nameof(YouTubeChipVisible));
|
|
OnPropertyChanged(nameof(LocalMp4ChipVisible));
|
|
OnPropertyChanged(nameof(ShowLocalMp4Selected));
|
|
OnPropertyChanged(nameof(ShowStartRecordingAction));
|
|
OnPropertyChanged(nameof(ShowEndRecordingAction));
|
|
OnPropertyChanged(nameof(RecordChipText));
|
|
}
|
|
|
|
/// <summary>Switches worlds. Re-entering the current one still re-asserts its edit state, which is
|
|
/// what lets a creator take EDIT back mid-show by clicking Build Scenes.</summary>
|
|
internal void EnterWorld(BarWorld world)
|
|
{
|
|
_barWorld = world;
|
|
|
|
// Entering a world starts with NOTHING chosen inside it (creator, build 729): the output
|
|
// switches are a fresh decision each time, not a remembered one.
|
|
if (world == BarWorld.Output)
|
|
{
|
|
RecordPillOn = false;
|
|
OnAirPillOn = false;
|
|
}
|
|
|
|
OnPropertyChanged(nameof(BarWorld));
|
|
OnPropertyChanged(nameof(IsBuildWorld));
|
|
OnPropertyChanged(nameof(IsOutputWorld));
|
|
EditingActive = world == BarWorld.Build;
|
|
OnPropertyChanged(nameof(ShowOutputStage));
|
|
RaiseOutputStage();
|
|
}
|
|
}
|