Creator challenge (2026-11-05): "I really don't think that's going to be a constant — it's going to
depend on so many variables, some that are dark to us." Correct, so the measured-grace constant and
the countdown are dropped from the spec. YouTube's cutoff is server-side and depends on ingest health,
prior stream quality, region and load; a pinned number would be a guess wearing a timer, and it would
be wrong exactly when the creator is staring at it.
What ships instead:
- **Amber RECONNECTING** in the top bar with measured facts only: elapsed seconds and the attempt
number ("0:12 · attempt 2 of 4"). The seconds come from a stopwatch started at the drop.
- **The status poll speeds up while recovering** (30 s → 4 s) and drives the sign; it restores 30 s
when recovery ends.
- **`chatEndedEvent`** — already in the chat feed we poll and previously skipped — now raises a signal
that prompts an immediate status check. It never decides anything: `liveBroadcasts.list`
`lifeCycleStatus` stays authoritative, with `liveStreams.list` health as the push-received signal.
- At `complete`: red OFF AIR (the existing stop path) rather than a pretend-live sign.
Tests: `ChatEndedSignalTests` (the marker is not a chat row, the service raises the signal, and a
complete broadcast ends recovery with the honest message). FAST gate: 543 passed, 0 warnings.
166 lines
11 KiB
XML
166 lines
11 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>
|
|
|
|
<!-- EDIT: explicit toggle. Neutral = off (chat), blue = on (layers/properties). The
|
|
trigger used to set the SAME colour as the resting state, so the button never
|
|
looked pressed. Right edge aligns with the layers panel below. -->
|
|
<ToggleButton Grid.Column="1" IsChecked="{Binding EditingActive, Mode=TwoWay}" Content="EDIT"
|
|
Cursor="Hand" Height="32" Padding="14,0" Margin="18,0,0,2"
|
|
HorizontalAlignment="Right" VerticalAlignment="Center"
|
|
FontWeight="Bold" FontSize="12" Foreground="{StaticResource Brush.OnAccent}" BorderThickness="0">
|
|
<ToggleButton.Template>
|
|
<ControlTemplate TargetType="ToggleButton">
|
|
<Border x:Name="EditBody" Background="{StaticResource Brush.SurfaceContainerHigh}" CornerRadius="4" Padding="{TemplateBinding Padding}">
|
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
</Border>
|
|
<ControlTemplate.Triggers>
|
|
<Trigger Property="IsChecked" Value="True">
|
|
<Setter TargetName="EditBody" Property="Background" Value="{StaticResource Brush.Tertiary}"/>
|
|
</Trigger>
|
|
</ControlTemplate.Triggers>
|
|
</ControlTemplate>
|
|
</ToggleButton.Template>
|
|
</ToggleButton>
|
|
</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">
|
|
<!-- IDLE: the mode switch + the action(s) of the armed world. The three
|
|
controls share one height (build 77 feedback) so they read as one
|
|
family — segments host, Start/Go Live, and Test all sit at 32. -->
|
|
<StackPanel Orientation="Horizontal"
|
|
Visibility="{Binding ShowIdleWorld, Converter={StaticResource BoolToVis}}">
|
|
<Border Background="{StaticResource Brush.SurfaceContainerLowest}" CornerRadius="4" Padding="2" Height="32" VerticalAlignment="Center">
|
|
<StackPanel Orientation="Horizontal">
|
|
<ToggleButton Style="{StaticResource RecordPillToggle}"
|
|
IsChecked="{Binding RecordPillOn}">
|
|
<!-- the label follows the pill's own foreground: muted when armed-off,
|
|
dark on red when armed (SegmentLabel pins its own colour) -->
|
|
<TextBlock Text="REC" FontSize="12" FontWeight="Bold" VerticalAlignment="Center"
|
|
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"/>
|
|
</ToggleButton>
|
|
<ToggleButton Style="{StaticResource RecordPillToggle}"
|
|
IsChecked="{Binding OnAirPillOn}">
|
|
<TextBlock Text="ON-AIR" FontSize="12" FontWeight="Bold" VerticalAlignment="Center"
|
|
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"/>
|
|
</ToggleButton>
|
|
</StackPanel>
|
|
</Border>
|
|
<!-- Go face names the world: "Start Recording" vs "Go Live".
|
|
(Signed-out ON-AIR Start routes to the sign-in — StartSession.) -->
|
|
<Button Content="{Binding PrimaryStartButtonLabel}" Style="{StaticResource YtButton}"
|
|
Height="32" Margin="10,0,0,0" Command="{Binding PrimaryStartCommand}"
|
|
Visibility="{Binding ShowPrimaryStartButton, Converter={StaticResource BoolToVis}}"/>
|
|
<!-- No separate TEST button (creator ruling 2026-10-30): EDIT decides whether
|
|
the ON-AIR start is the real livestream or the test one. -->
|
|
</StackPanel>
|
|
|
|
<!-- 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> |