Files
LlamaCasty/Controls/TopBar.xaml
T
gramps ba60f9ab3d feat(topbar): three activities, always visible — build, stream, record
Creator ruling 2026-11-13: "the app does three things. builds. records. live-streams." So the bar holds
three buttons that never disappear; the active one is lit and runs its own flow in place, and the other
two dim but stay clickable, so there is no state where the creator cannot see where they are or how to
step back.

  Build    [Build Scenes]  -> editing on, nothing to start
  YouTube  [Live][Private] -> [Start] -> [Stop]     (Private = the private-visibility test stream)
  Record   [Start Recording] -> [● REC hh:mm:ss][Stop Recording]

Arming and starting are separate presses on purpose (creator): Start is the last chance to tell someone
"check you are ready". Collapsing them would save one click and lose that prompt.

This replaces the two-world model (BarWorld) and its output stage outright, including the chips added
earlier today. The old pills remain the underlying state (OnAirPillOn/RecordPillOn) so the start, stop and
step-down paths are untouched; the activity only decides what the bar shows.

Closing the app no longer waits 6 s for a session that cannot stop: it waits ~2 s, then clears the state
and closes anyway. A window that refuses to close is worse than a stale flag — and that refusal is what
hung the suite in ZoomBoxInteractionTests, which sets IsRecording and closes a real window.

Test: TopBarActivityTests pins all three flows, the arm-then-start split, and that stepping back to Build
forgets the YouTube choice instead of carrying it. TopBarWorldTests is retired with the model it tested.
2026-11-13 11:30:57 -08:00

238 lines
18 KiB
XML

<UserControl x:Class="ytLive.Controls.TopBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:helpers="clr-namespace:ytLive.Helpers"
x:Name="TopBarRoot">
<Grid>
<Grid.ColumnDefinitions>
<!-- 352 = the layers panel's right edge in the work area below: window margin 8 +
360px column = 368, minus this bar's own 16px padding. EDIT is right-aligned in
this column so the two edges line up exactly (creator ruling 2026-11-01). -->
<ColumnDefinition Width="352"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- BRAND + EDIT. The gear moved to the preview's right rail, first tab (2026-11-01):
app-level actions live with the other pull-outs now. -->
<Grid Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- LOGO / BRAND — left-click opens the in-app About overlay; right-click opens the app
menu (Report Bug / Feature Request / About). App Settings is no longer in this
menu: the gear rail tab opens the Settings drawer directly (creator ruling
2026-11-01). -->
<Button Grid.Column="0" Background="Transparent" BorderThickness="0" Cursor="Hand"
HorizontalAlignment="Left"
VerticalAlignment="Center" ToolTip="About LlamaCasty — right-click for the menu"
Command="{Binding OpenAboutCommand}"
PreviewMouseRightButtonDown="BrandWordmark_PreviewMouseRightButtonDown">
<Button.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Report Bug" Command="{Binding OpenBugCommand}"/>
<MenuItem Header="Feature Request" Command="{Binding OpenFeatureCommand}"/>
<MenuItem Header="About" Command="{Binding OpenAboutCommand}"/>
</ContextMenu>
</Button.ContextMenu>
<Button.Template>
<ControlTemplate TargetType="Button">
<ContentPresenter/>
</ControlTemplate>
</Button.Template>
<!-- Wordmark: "LlamaCas" + "ty" in YouTube red (creator ruling 2026-10-30 — never
re-colour it). The Run elements MUST stay adjacent: whitespace between inline
Runs renders as a space and splits the name into "LlamaCas ty". -->
<TextBlock FontSize="22" FontWeight="Bold" VerticalAlignment="Center">
<Run Text="LlamaCas" Foreground="{StaticResource Brush.OnSurface}"/><Run Text="ty" Foreground="{StaticResource Brush.Youtube}"/><Run
x:Name="BuildIdStamp" Text="{x:Static helpers:BuildStamp.Label}" FontSize="9"
BaselineAlignment="Superscript" Foreground="{StaticResource Brush.OnSurfaceVariant}"
ToolTipService.ToolTip="{x:Static helpers:BuildStamp.Label}"/><Run Text=" " FontSize="4"/>
</TextBlock>
</Button>
</Grid>
<!-- Session surface (top bar redesign): the bar is ONE surface rendered by
the FIRST decision — Record or Stream. Each world is the whole bar; the
other world never exists on screen. While running, the bar drops to a
reality line + End so nothing mid-session can be re-armed. -->
<StackPanel Grid.Column="1" Orientation="Horizontal"
HorizontalAlignment="Center" VerticalAlignment="Center">
<!-- THE decision (creator ruling 2026-11-12): [ Build Scenes ][ Record / Go Live ].
Build Scenes is the new edit mode — there is no separate EDIT button, so the
question "which world am I in?" and "can I edit?" cannot disagree. Clicking the
highlighted half re-enters it, which is how a creator gets back to the layers
during a session. -->
<!-- THREE ACTIVITIES, ALWAYS VISIBLE (creator, 2026-11-13): build, stream, record. The
active one is lit; the others stay put and dim, so the creator can see where they are
and step back without hunting. Each active button runs its own flow in place:
Build nothing to start
YouTube [Live][Private] -> Start -> Stop (arming and starting are separate presses:
Start is the last chance to check you are ready)
Record Start Recording -> Stop Recording
-->
<Border Background="{StaticResource Brush.SegmentTrack}" CornerRadius="12" Padding="4"
VerticalAlignment="Center" Margin="0,0,12,0"
KeyboardNavigation.TabNavigation="Cycle" KeyboardNavigation.DirectionalNavigation="Cycle">
<StackPanel Orientation="Horizontal">
<!-- 1. BUILD SCENES -->
<Button x:Name="BuildSegmentButton" Style="{StaticResource SegmentHalf}"
Tag="{Binding IsBuildActivity}" Command="{Binding SelectBuildActivityCommand}"
ToolTip="Edit layers, scenes and webcam placement">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource SegmentIcon}" Tag="{Binding IsBuildActivity}" Data="M3,4h8v6H3V4zM13,4h8v6h-8V4zM3,12h8v8H3V12zM13,12h8v8h-8V12z"/>
<TextBlock Text="Build Scenes" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</StackPanel>
</Button>
<!-- 2. YOUTUBE -->
<Button x:Name="YouTubeActivityButton" Style="{StaticResource SegmentHalf}"
Tag="{Binding IsYouTubeActivity}" Command="{Binding SelectYouTubeActivityCommand}"
ToolTip="Stream to YouTube">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource SegmentIcon}" Tag="{Binding IsYouTubeActivity}" Data="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
<TextBlock Text="YouTube" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</StackPanel>
</Button>
<!-- YouTube's own flow, in place. -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
Visibility="{Binding ShowYouTubeModeChoices, Converter={StaticResource BoolToVis}}">
<Button Style="{StaticResource OutputChip}" Tag="{Binding IsLiveModeChosen}"
Command="{Binding ChooseLiveModeCommand}" ToolTip="Public livestream">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource OutputChipIcon}" Tag="{Binding IsLiveModeChosen}" Data="M8,5v14l11,-7z"/>
<TextBlock Text="Live" FontSize="12" Margin="5,0,0,0" VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</StackPanel>
</Button>
<Button Style="{StaticResource OutputChip}" Tag="{Binding IsPrivateModeChosen}"
Command="{Binding ChoosePrivateModeCommand}" ToolTip="Private stream — nothing public is published">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource OutputChipIcon}" Tag="{Binding IsPrivateModeChosen}" Data="M7,10V7a5,5 0 0,1 10,0v3h1a1,1 0 0,1 1,1v9a1,1 0 0,1 -1,1H6a1,1 0 0,1 -1,-1v-9a1,1 0 0,1 1,-1h1zM9,10h6V7a3,3 0 0,0 -6,0v3z"/>
<TextBlock Text="Private" FontSize="12" Margin="5,0,0,0" VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</StackPanel>
</Button>
</StackPanel>
<Button Style="{StaticResource OutputChip}" Command="{Binding StartChosenStreamCommand}"
Visibility="{Binding ShowYouTubeStart, Converter={StaticResource BoolToVis}}"
ToolTip="Start the stream you armed">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource OutputChipIcon}" Data="M8,5v14l11,-7z"/>
<TextBlock Text="Start" FontSize="12" FontWeight="Bold" Margin="5,0,0,0" VerticalAlignment="Center"
Foreground="{StaticResource Brush.SegmentTextActive}"/>
</StackPanel>
</Button>
<Button Style="{StaticResource OutputChip}" Command="{Binding EndStreamCommand}"
Visibility="{Binding ShowYouTubeStop, Converter={StaticResource BoolToVis}}"
ToolTip="End the livestream">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource OutputChipIcon}" Data="M6,6h12v12H6z"/>
<TextBlock Text="Stop" FontSize="12" FontWeight="Bold" Margin="5,0,0,0" VerticalAlignment="Center"
Foreground="{StaticResource Brush.SegmentTextActive}"/>
</StackPanel>
</Button>
<!-- 3. RECORD -->
<Button x:Name="RecordActivityButton" Style="{StaticResource SegmentHalf}"
Tag="{Binding IsRecordActivity}" Command="{Binding SelectRecordActivityCommand}"
ToolTip="Record to a local MP4">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource SegmentIcon}" Tag="{Binding IsRecordActivity}" Data="M12,2a10,10 0 1,0 0,20a10,10 0 1,0 0,-20zM12,4a8,8 0 1,1 0,16a8,8 0 1,1 0,-16zM12,7.5a4.5,4.5 0 1,1 0,9a4.5,4.5 0 1,1 0,-9z"/>
<TextBlock Text="Record" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</StackPanel>
</Button>
<!-- Record's own flow: the clock while it runs, and one action that swaps. -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
Visibility="{Binding IsRecordActivity, Converter={StaticResource BoolToVis}}">
<StackPanel Orientation="Horizontal" Margin="2,0,6,0" VerticalAlignment="Center"
Visibility="{Binding IsRecording, Converter={StaticResource BoolToVis}}">
<Ellipse Width="10" Height="10" VerticalAlignment="Center"
Fill="{StaticResource Brush.RecordLive}"
Opacity="{Binding RecDotOpacity, Mode=OneWay}"/>
<TextBlock Text="{Binding RecordElapsedLabel}" FontSize="12" FontWeight="Bold"
FontFamily="Consolas" Margin="5,0,0,0" VerticalAlignment="Center"
Foreground="{StaticResource Brush.SegmentTextActive}"/>
</StackPanel>
<Button Style="{StaticResource OutputChip}" Command="{Binding StartRecordingCommand}"
Visibility="{Binding ShowRecordStart, Converter={StaticResource BoolToVis}}">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource OutputChipIcon}" Data="M12,2a10,10 0 1,0 0,20a10,10 0 1,0 0,-20zM12,4a8,8 0 1,1 0,16a8,8 0 1,1 0,-16zM12,7.5a4.5,4.5 0 1,1 0,9a4.5,4.5 0 1,1 0,-9z"/>
<TextBlock Text="Start Recording" FontSize="12" FontWeight="Bold" Margin="5,0,0,0"
VerticalAlignment="Center" Foreground="{StaticResource Brush.SegmentTextActive}"/>
</StackPanel>
</Button>
<Button Style="{StaticResource OutputChip}" Command="{Binding EndStreamCommand}"
Visibility="{Binding ShowRecordStop, Converter={StaticResource BoolToVis}}">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource OutputChipIcon}" Data="M6,6h12v12H6z"/>
<TextBlock Text="Stop Recording" FontSize="12" FontWeight="Bold" Margin="5,0,0,0"
VerticalAlignment="Center" Foreground="{StaticResource Brush.SegmentTextActive}"/>
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
</Border>
<!-- RUNNING: one reality line (dot + word + elapsed) and the stop -->
<StackPanel Orientation="Horizontal"
Visibility="{Binding RunningVisible, Converter={StaticResource BoolToVis}}">
<Ellipse Width="12" Height="12" VerticalAlignment="Center"
Fill="{Binding RunningDotBrush}"/>
<TextBlock Text="{Binding RunningText}" FontSize="16" FontWeight="Bold"
Foreground="White" Margin="8,0,0,0" VerticalAlignment="Center"/>
<TextBlock Text="{Binding LiveElapsedText}" Foreground="White" FontSize="14"
FontFamily="Consolas" VerticalAlignment="Center" Margin="8,0,0,0"/>
<!-- TASK 32·2: measured facts only — elapsed seconds and which attempt. No countdown:
YouTube's cutoff is not a constant we can promise. -->
<TextBlock x:Name="ReconnectDetail" Text="{Binding ReconnectDetailText}"
Foreground="{StaticResource Brush.Attention}" FontSize="13"
FontFamily="Consolas" VerticalAlignment="Center" Margin="10,0,0,0"
Visibility="{Binding IsReconnecting, Converter={StaticResource BoolToVis}}"/>
<Button Content="{Binding EndButtonText}" Style="{StaticResource YtButton}"
Background="{StaticResource Brush.Primary}" Margin="14,0,0,0"
Command="{Binding EndStreamCommand}"/>
</StackPanel>
</StackPanel>
<!-- Account zone: lives only in the Stream world. Signed out → Sign In;
signed in → a big avatar of the streaming account (build 77 feedback:
"make the user icon much bigger"). Record world has no YouTube
identity at all. -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<Button Content="Sign In" Style="{StaticResource YtButtonSecondary}"
Margin="0,0,8,0" Command="{Binding SignInCommand}"
ToolTip="Sign in to YouTube to go live."
Visibility="{Binding ShowSignInButton, Converter={StaticResource BoolToVis}}"/>
<Button Style="{StaticResource IconButton}" Margin="0,0,0,0"
ToolTip="{Binding AccountDisplayName}"
Visibility="{Binding ShowAvatarButton, Converter={StaticResource BoolToVis}}">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="Change Account" Command="{Binding ChangeAccountCommand}"/>
<Separator/>
<MenuItem Header="Logout" Command="{Binding LogoutCommand}"/>
</ContextMenu>
</Button.ContextMenu>
<Border Width="40" Height="40" CornerRadius="20" Background="{StaticResource Brush.SurfaceContainer}"
ClipToBounds="True" BorderBrush="{StaticResource Brush.Outline}" BorderThickness="1">
<Grid>
<Image x:Name="AvatarImage" Stretch="UniformToFill"/>
<TextBlock x:Name="AvatarInitial" Foreground="{StaticResource Brush.OnSurface}" FontSize="20"
FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center"
Visibility="Collapsed"/>
</Grid>
</Border>
</Button>
</StackPanel>
</Grid>
</UserControl>