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.
257 lines
20 KiB
XML
257 lines
20 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. -->
|
|
<!-- The segmented mode switcher (creator mock 2026-11-12): pill track, two equal halves,
|
|
active half on a lighter pill with off-white bold text and a coral icon. Hover tints
|
|
only the inactive half. -->
|
|
<Border Background="{StaticResource Brush.SegmentTrack}" CornerRadius="12" Padding="4"
|
|
VerticalAlignment="Center" Margin="0,0,12,0"
|
|
KeyboardNavigation.TabNavigation="Cycle" KeyboardNavigation.DirectionalNavigation="Cycle"
|
|
Visibility="{Binding IsBuildWorld, Converter={StaticResource BoolToVis}}">
|
|
<Grid>
|
|
<Grid.ColumnDefinitions>
|
|
<ColumnDefinition Width="*"/>
|
|
<ColumnDefinition Width="*"/>
|
|
</Grid.ColumnDefinitions>
|
|
<Button x:Name="BuildSegmentButton" Grid.Column="0" Style="{StaticResource SegmentHalf}"
|
|
Tag="{Binding IsBuildWorld}" Command="{Binding EnterBuildWorldCommand}"
|
|
ToolTip="Edit layers, scenes and webcam placement">
|
|
<StackPanel Orientation="Horizontal">
|
|
<Path Style="{StaticResource SegmentIcon}" Tag="{Binding IsBuildWorld}"
|
|
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>
|
|
<Button x:Name="OutputSegmentButton" Grid.Column="1" Style="{StaticResource SegmentHalf}"
|
|
Tag="{Binding IsOutputWorld}" Command="{Binding EnterOutputWorldCommand}"
|
|
ToolTip="Choose the output and start the session">
|
|
<StackPanel Orientation="Horizontal">
|
|
<Path Style="{StaticResource SegmentIcon}" Tag="{Binding IsOutputWorld}"
|
|
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 / Go-Live" FontSize="12" Margin="6,0,0,0"
|
|
VerticalAlignment="Center"
|
|
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
|
|
</StackPanel>
|
|
</Button>
|
|
</Grid>
|
|
</Border>
|
|
<!-- 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. -->
|
|
<!-- THE OUTPUT STAGE (creator, builds 728/729). Three states:
|
|
1. nothing chosen: [YouTube] [Local MP4]
|
|
2. YouTube chosen: [YouTube(red)] [Live Stream] [Private Stream]
|
|
3. Local MP4 chosen: [Local MP4(red)] [Start Recording] -> [End Recording]
|
|
"Private Stream" is the private-visibility stream, which is what this app has always
|
|
called the test stream. The action chips are never highlighted; only the chosen output
|
|
is red. -->
|
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
|
|
Visibility="{Binding ShowOutputStage, Converter={StaticResource BoolToVis}}">
|
|
<TextBlock Text="Output:" FontSize="11" VerticalAlignment="Center"
|
|
Foreground="{StaticResource Brush.OnSurfaceVariant}" Margin="0,0,8,0"/>
|
|
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
|
|
<Border Background="{StaticResource Brush.SegmentTrack}" CornerRadius="12" Padding="4"
|
|
VerticalAlignment="Center">
|
|
<StackPanel Orientation="Horizontal">
|
|
<!-- The chosen output, or the two choices while none is chosen. -->
|
|
<Button Style="{StaticResource OutputChip}" Tag="{Binding OnAirPillOn}"
|
|
Command="{Binding SelectYouTubeCommand}"
|
|
Visibility="{Binding YouTubeChipVisible, Converter={StaticResource BoolToVis}}"
|
|
ToolTip="Stream to YouTube">
|
|
<StackPanel Orientation="Horizontal">
|
|
<Path Style="{StaticResource OutputChipIcon}" Tag="{Binding OnAirPillOn}" 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>
|
|
<Button Style="{StaticResource OutputChip}" Tag="{Binding RecordPillOn}"
|
|
Command="{Binding SelectLocalMp4Command}"
|
|
Visibility="{Binding LocalMp4ChipVisible, Converter={StaticResource BoolToVis}}"
|
|
ToolTip="Record to a local MP4">
|
|
<Grid>
|
|
<!-- Idle face: the record glyph and the output's name. -->
|
|
<StackPanel Orientation="Horizontal"
|
|
Visibility="{Binding IsRecording, Converter={StaticResource InverseBoolToVis}}">
|
|
<Path Style="{StaticResource OutputChipIcon}" Tag="{Binding RecordPillOn}" 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="Local MP4" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
|
|
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
|
|
</StackPanel>
|
|
<!-- Recording face: solid green dot pulsing once a second, and a tabular
|
|
clock so the pill does not jitter as the digits change. -->
|
|
<StackPanel Orientation="Horizontal"
|
|
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 RecordChipText}" FontSize="12" FontWeight="Bold"
|
|
Margin="6,0,0,0" VerticalAlignment="Center"
|
|
FontFamily="Consolas"
|
|
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
|
|
</StackPanel>
|
|
</Grid>
|
|
</Button>
|
|
<!-- YouTube's two actions: the real stream and the private (test) one. -->
|
|
<Button Style="{StaticResource OutputChip}" Command="{Binding GoLiveCommand}"
|
|
Visibility="{Binding ShowYouTubeSelected, Converter={StaticResource BoolToVis}}"
|
|
ToolTip="Go live on YouTube">
|
|
<StackPanel Orientation="Horizontal">
|
|
<Path Style="{StaticResource OutputChipIcon}" Data="M8,5v14l11,-7z"/>
|
|
<TextBlock Text="Live Stream" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
|
|
Foreground="{StaticResource Brush.SegmentTextInactive}"/>
|
|
</StackPanel>
|
|
</Button>
|
|
<Button Style="{StaticResource OutputChip}" Command="{Binding TestStreamCommand}"
|
|
Visibility="{Binding ShowYouTubeSelected, Converter={StaticResource BoolToVis}}"
|
|
ToolTip="Private test stream — nothing public is published">
|
|
<StackPanel Orientation="Horizontal">
|
|
<Path Style="{StaticResource OutputChipIcon}" 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 Stream" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
|
|
Foreground="{StaticResource Brush.SegmentTextInactive}"/>
|
|
</StackPanel>
|
|
</Button>
|
|
<!-- Local MP4's one action, which becomes End while it records. -->
|
|
<Button Style="{StaticResource OutputChip}" Command="{Binding StartRecordingCommand}"
|
|
Visibility="{Binding ShowStartRecordingAction, 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" Margin="6,0,0,0" VerticalAlignment="Center"
|
|
Foreground="{StaticResource Brush.SegmentTextInactive}"/>
|
|
</StackPanel>
|
|
</Button>
|
|
<Button Style="{StaticResource OutputChip}" Command="{Binding EndStreamCommand}"
|
|
Visibility="{Binding ShowEndRecordingAction, Converter={StaticResource BoolToVis}}">
|
|
<StackPanel Orientation="Horizontal">
|
|
<Path Style="{StaticResource OutputChipIcon}" Data="M6,6h12v12H6z"/>
|
|
<TextBlock Text="Stop Recording" FontSize="12" Margin="6,0,0,0" VerticalAlignment="Center"
|
|
Foreground="{StaticResource Brush.SegmentTextInactive}"/>
|
|
</StackPanel>
|
|
</Button>
|
|
</StackPanel>
|
|
</Border>
|
|
<TextBlock Text="<esc> to return" FontSize="10" HorizontalAlignment="Center"
|
|
Foreground="{StaticResource Brush.OnSurfaceVariant}" Margin="0,2,0,0"/>
|
|
</StackPanel>
|
|
<TextBlock Text="One output at a time" FontSize="11" VerticalAlignment="Center"
|
|
Foreground="{StaticResource Brush.OnSurfaceVariant}" Margin="8,0,0,0"/>
|
|
</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> |