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.
238 lines
18 KiB
XML
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> |