f5a9d46881
The credit existed only as a WPF BrandFlashLayer TextBlock in the preview at 25% opacity on a 300s timer. A viewer of the stream or the recording never saw it, so "free tier shows branding" was not actually being delivered. The frame pump has carried an unused per-frame flashFrame slot for exactly this. Reverses the documented "Pre-GA posture" (ai.md Monetization), which kept the flash preview-only so test VODs stayed clean. Creator ruling 2026-09-26: the free tier has to be honest advertising, so it now reaches the broadcast. One rendered VideoFrame feeds BOTH the frame pump and the preview, so the creator's view cannot drift from what viewers get. Spec: 500ms in / 1000ms hold / 500ms out, first credit ~5s after go-live then every rand(30s)+30s, single unwrapped line at a random spot inside the frame every time, neon white core with a feathered red/blue halo. Neon recipe is derivative work, per AGENTS.md: bright core + feathered multi-radius halo with R and B split in opposite directions, from https://nudaui.dev/components/neon-glow (layered text-shadow falloff), https://help.maxon.net/rg/en-us/Content/html/Blurs-and-Glows-chromatic-glow.html ("with no displacement, the green channel is all but invisible behind your white text" once R and B are split) and the OBS obs-stroke-glow-shadow "feathered stroke with user-defined size and intensity". Neon blue is #4d8bff rather than the theme's #0f3460, which renders near-black as a halo. Also fixes a PRE-EXISTING bug found on the way: FramePump's fully-static shortcut stretched the cached bake and returned it, silently discarding every per-frame overlay - the social bar and the alert ticker were already lost there. SceneCompositor.Overlay is now internal (it copies before blitting, so the bake is never mutated) and the shortcut composites overlays first. The credit is deliberately NOT folded into SceneGraph.GetBakedBase: SceneRegion.Matches compares element ids only, so a credit in the cached bake would freeze there and never expire. Per-frame only, and Epoch is stamped every read because the 8MB master buffer is recycled - without that the paste cache would freeze a stale credit. BrandFlashEnabled is now derived !IsPremium and non-assignable, and the presenter re-checks the licence on every read, so a key entered mid-credit cuts the advertisement on the next tick instead of letting it finish. Tests: 10 new facts. 357 total, 356 pass; the one failure is the known environment-flaky RealMouseDrag layer test (needs an uncovered desktop session). Adds RealAppHost.RunAsync - a frame-pump test must await inside the collection's shared STA thread or the whole suite deadlocks silently. NOTE: scope-check.sh flags Helpers/InstanceProfile.cs, AppLog.cs, TokenStore.cs, WebView2Manager.cs and InstanceIsolationTests.cs as outside this commit. That is a false positive: it uses `git diff HEAD`, which cannot distinguish a planned second commit in the same session from an unrelated edit. Those files are the dev multi-instance unit, committed immediately after this one - as is the InstanceProfile paragraph in ai.md, which shares a file with the Monetization section corrected here.
953 lines
86 KiB
XML
953 lines
86 KiB
XML
<UserControl x:Class="ytLive.Controls.PreviewPane"
|
||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||
xmlns:Helpers="clr-namespace:ytLive.Helpers">
|
||
<Border Background="#0a0a1a" CornerRadius="6" Margin="8,0"
|
||
BorderBrush="{Binding PreviewGlowBrush}" BorderThickness="{Binding PreviewGlowThickness}">
|
||
<Grid>
|
||
<Grid.RowDefinitions>
|
||
<RowDefinition Height="*"/>
|
||
<RowDefinition Height="Auto"/>
|
||
</Grid.RowDefinitions>
|
||
|
||
<!-- Preview monitor -->
|
||
<Grid x:Name="PreviewGrid" Grid.Row="0"
|
||
PreviewMouseLeftButtonDown="Preview_MouseLeftButtonDown"
|
||
PreviewMouseMove="Preview_MouseMove"
|
||
PreviewMouseLeftButtonUp="Preview_MouseLeftButtonUp">
|
||
<Viewbox Stretch="Uniform">
|
||
<Grid x:Name="CanvasGrid" Width="1920" Height="1080" ClipToBounds="True" Background="Transparent">
|
||
<Grid.ContextMenu>
|
||
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
||
<MenuItem Header="Show Desktop" IsCheckable="True"
|
||
IsChecked="{Binding BackgroundShowDesktop}"
|
||
Visibility="{Binding CanChangeBackground, Converter={StaticResource BoolToVis}}"
|
||
ToolTip="Show desktop when no fullscreen game is detected"/>
|
||
<MenuItem Header="Capture Desktop"
|
||
Visibility="{Binding CanChangeBackground, Converter={StaticResource BoolToVis}}"
|
||
ItemsSource="{Binding Displays}">
|
||
<MenuItem.ItemContainerStyle>
|
||
<Style TargetType="MenuItem">
|
||
<Setter Property="Header" Value="{Binding Label}"/>
|
||
<Setter Property="Command"
|
||
Value="{Binding DataContext.SetBackgroundDisplayCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
|
||
<Setter Property="CommandParameter" Value="{Binding}"/>
|
||
</Style>
|
||
</MenuItem.ItemContainerStyle>
|
||
</MenuItem>
|
||
<MenuItem Header="Capture Window…"
|
||
Visibility="{Binding CanChangeBackground, Converter={StaticResource BoolToVis}}"
|
||
ItemsSource="{Binding OpenWindows}">
|
||
<MenuItem.ItemContainerStyle>
|
||
<Style TargetType="MenuItem">
|
||
<Setter Property="Header" Value="{Binding Label}"/>
|
||
<Setter Property="Command"
|
||
Value="{Binding DataContext.CaptureWindowCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
|
||
<Setter Property="CommandParameter" Value="{Binding}"/>
|
||
</Style>
|
||
</MenuItem.ItemContainerStyle>
|
||
</MenuItem>
|
||
<MenuItem Header="Refresh Window List" Command="{Binding RefreshWindowsCommand}"
|
||
Visibility="{Binding CanChangeBackground, Converter={StaticResource BoolToVis}}"/>
|
||
<MenuItem Header="Refresh Desktop" Command="{Binding RefreshCaptureCommand}"
|
||
Visibility="{Binding CanChangeBackground, Converter={StaticResource BoolToVis}}"/>
|
||
</ContextMenu>
|
||
</Grid.ContextMenu>
|
||
<!-- Live desktop/game background (bottom layer; the static
|
||
image renders above it). -->
|
||
<Image Source="{Binding BackgroundImage}" Stretch="UniformToFill"
|
||
Visibility="{Binding BackgroundVisible, Converter={StaticResource BoolToVis}}"
|
||
IsHitTestVisible="False"/>
|
||
<Image Source="{Binding ActiveBackgroundImage}" Stretch="UniformToFill"
|
||
IsHitTestVisible="False"/>
|
||
<Rectangle Stroke="#22c55e" StrokeThickness="6" IsHitTestVisible="False"/>
|
||
<Canvas x:Name="OverlayCanvas" Width="1920" Height="1080">
|
||
<ItemsControl ItemsSource="{Binding StagedScene.Elements}"
|
||
Width="1920" Height="1080">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate>
|
||
<Canvas/>
|
||
</ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemContainerStyle>
|
||
<Style TargetType="ContentPresenter">
|
||
<Setter Property="Canvas.Left" Value="{Binding X}"/>
|
||
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
|
||
</Style>
|
||
</ItemsControl.ItemContainerStyle>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate>
|
||
<Grid Width="{Binding Width}" Height="{Binding Height}"
|
||
Opacity="{Binding Opacity}"
|
||
Background="Transparent"
|
||
RenderTransformOrigin="0.5,0.5">
|
||
<Grid.Style>
|
||
<Style TargetType="Grid">
|
||
<Setter Property="Visibility" Value="Collapsed"/>
|
||
<Style.Triggers>
|
||
<DataTrigger Binding="{Binding IsVisible}" Value="True">
|
||
<Setter Property="Visibility" Value="Visible"/>
|
||
</DataTrigger>
|
||
<!-- The background is a fixed full-frame layer (rendered separately,
|
||
below the static background): never hit-testable, so it can't be
|
||
selected, dragged, or bring up the webcam context menu. -->
|
||
<DataTrigger Binding="{Binding IsBackground}" Value="True">
|
||
<Setter Property="IsHitTestVisible" Value="False"/>
|
||
</DataTrigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
</Grid.Style>
|
||
<Grid.RenderTransform>
|
||
<ScaleTransform ScaleX="{Binding MirrorScale}"/>
|
||
</Grid.RenderTransform>
|
||
<Image Source="{Binding DisplaySource}"
|
||
RenderOptions.BitmapScalingMode="HighQuality">
|
||
<Image.Style>
|
||
<Style TargetType="Image">
|
||
<Setter Property="Visibility" Value="Collapsed"/>
|
||
<Setter Property="Stretch" Value="UniformToFill"/>
|
||
<Style.Triggers>
|
||
<DataTrigger Binding="{Binding IsImageSource}" Value="True">
|
||
<Setter Property="Visibility" Value="Visible"/>
|
||
</DataTrigger>
|
||
<DataTrigger Binding="{Binding IsChatBox}" Value="True">
|
||
<Setter Property="Visibility" Value="Visible"/>
|
||
</DataTrigger>
|
||
<!-- TASK 47: the alert box's frame (the decoded clip) is written onto
|
||
VideoImageSource exactly like the chat box's, so it needs the same
|
||
trigger. Without it the Image stayed Collapsed and every alert was
|
||
invisible in the preview while the decode logged healthy (2026-09-26). -->
|
||
<DataTrigger Binding="{Binding IsAlertBox}" Value="True">
|
||
<Setter Property="Visibility" Value="Visible"/>
|
||
</DataTrigger>
|
||
<DataTrigger Binding="{Binding IsWebSource}" Value="True">
|
||
<Setter Property="Visibility" Value="Visible"/>
|
||
<Setter Property="Stretch" Value="Fill"/>
|
||
</DataTrigger>
|
||
<MultiDataTrigger>
|
||
<MultiDataTrigger.Conditions>
|
||
<Condition Binding="{Binding IsWebcam}" Value="True"/>
|
||
<Condition Binding="{Binding ClipShape}" Value="Traditional"/>
|
||
</MultiDataTrigger.Conditions>
|
||
<Setter Property="Visibility" Value="Visible"/>
|
||
</MultiDataTrigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
</Image.Style>
|
||
</Image>
|
||
<Viewbox Stretch="Uniform">
|
||
<Grid Width="1" Height="1">
|
||
<!-- Round webcam: a plain Image (the same efficient WriteableBitmap
|
||
path as Traditional) clipped to a unit circle and scaled by the
|
||
Viewbox — deliberately NOT an ImageBrush, whose per-frame
|
||
re-rasterization at full scale stalls the preview. -->
|
||
<Image Stretch="UniformToFill"
|
||
Source="{Binding DisplaySource}"
|
||
RenderOptions.BitmapScalingMode="HighQuality">
|
||
<Image.Style>
|
||
<Style TargetType="Image">
|
||
<Setter Property="Visibility" Value="Collapsed"/>
|
||
<Style.Triggers>
|
||
<MultiDataTrigger>
|
||
<MultiDataTrigger.Conditions>
|
||
<Condition Binding="{Binding IsWebcam}" Value="True"/>
|
||
<Condition Binding="{Binding ClipShape}" Value="Round"/>
|
||
</MultiDataTrigger.Conditions>
|
||
<Setter Property="Visibility" Value="Visible"/>
|
||
</MultiDataTrigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
</Image.Style>
|
||
<Image.Clip>
|
||
<EllipseGeometry Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5"/>
|
||
</Image.Clip>
|
||
</Image>
|
||
</Grid>
|
||
</Viewbox>
|
||
|
||
<!-- OBS-style border: stroked rect (Traditional) / centered circle (Round).
|
||
Animations are driven by code-behind in WebcamMenu_SetBorderAnimation
|
||
because DataTrigger storyboards in DataTemplates cannot be stopped
|
||
(no ExitActions namescope support). -->
|
||
<Rectangle IsHitTestVisible="False"
|
||
Stroke="{Binding BorderBrush}"
|
||
StrokeThickness="{Binding BorderWidth}">
|
||
<Rectangle.Style>
|
||
<Style TargetType="Rectangle">
|
||
<Setter Property="Visibility" Value="Collapsed"/>
|
||
<Style.Triggers>
|
||
<MultiDataTrigger>
|
||
<MultiDataTrigger.Conditions>
|
||
<Condition Binding="{Binding HasBorder}" Value="True"/>
|
||
<Condition Binding="{Binding ClipShape}" Value="Traditional"/>
|
||
</MultiDataTrigger.Conditions>
|
||
<Setter Property="Visibility" Value="Visible"/>
|
||
</MultiDataTrigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
</Rectangle.Style>
|
||
</Rectangle>
|
||
<Ellipse IsHitTestVisible="False"
|
||
Stroke="{Binding BorderBrush}"
|
||
StrokeThickness="{Binding BorderWidth}"
|
||
Width="{Binding RoundBorderSize}"
|
||
Height="{Binding RoundBorderSize}"
|
||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||
<Ellipse.Style>
|
||
<Style TargetType="Ellipse">
|
||
<Setter Property="Visibility" Value="Collapsed"/>
|
||
<Style.Triggers>
|
||
<MultiDataTrigger>
|
||
<MultiDataTrigger.Conditions>
|
||
<Condition Binding="{Binding HasBorder}" Value="True"/>
|
||
<Condition Binding="{Binding ClipShape}" Value="Round"/>
|
||
</MultiDataTrigger.Conditions>
|
||
<Setter Property="Visibility" Value="Visible"/>
|
||
</MultiDataTrigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
</Ellipse.Style>
|
||
</Ellipse>
|
||
|
||
<Grid.ContextMenu>
|
||
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
||
<MenuItem Header="Change Webcam…" Click="WebcamMenu_ChangeWebcam"
|
||
Visibility="{Binding IsWebcam, Converter={StaticResource BoolToVis}}"/>
|
||
<Separator/>
|
||
<MenuItem Header="Border Thickness">
|
||
<StackPanel Orientation="Horizontal" Margin="8,2">
|
||
<TextBlock Text="Thickness" Foreground="#e0e0e0" VerticalAlignment="Center"/>
|
||
<Slider Width="120" Minimum="0" Maximum="20" Margin="8,0,0,0"
|
||
Value="{Binding BorderWidth, Mode=TwoWay}"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</MenuItem>
|
||
<MenuItem Header="Border Opacity">
|
||
<StackPanel Orientation="Horizontal" Margin="8,2">
|
||
<TextBlock Text="Opacity" Foreground="#e0e0e0" VerticalAlignment="Center"/>
|
||
<Slider Width="120" Minimum="0" Maximum="1" Margin="8,0,0,0"
|
||
Value="{Binding BorderOpacity, Mode=TwoWay}"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</MenuItem>
|
||
<MenuItem Header="Border Color">
|
||
<MenuItem Header="None" Tag="" IsCheckable="True"
|
||
IsChecked="{Binding BorderColor, Converter={StaticResource EnumToBool}, ConverterParameter=''}"
|
||
Click="WebcamMenu_SetBorderColor"/>
|
||
<MenuItem Header="#ffffff" Tag="#ffffff" IsCheckable="True"
|
||
IsChecked="{Binding BorderColor, Converter={StaticResource EnumToBool}, ConverterParameter=#ffffff}"
|
||
Click="WebcamMenu_SetBorderColor"/>
|
||
<MenuItem Header="#000000" Tag="#000000" IsCheckable="True"
|
||
IsChecked="{Binding BorderColor, Converter={StaticResource EnumToBool}, ConverterParameter=#000000}"
|
||
Click="WebcamMenu_SetBorderColor"/>
|
||
<MenuItem Header="#e94560" Tag="#e94560" IsCheckable="True"
|
||
IsChecked="{Binding BorderColor, Converter={StaticResource EnumToBool}, ConverterParameter=#e94560}"
|
||
Click="WebcamMenu_SetBorderColor"/>
|
||
<MenuItem Header="#22c55e" Tag="#22c55e" IsCheckable="True"
|
||
IsChecked="{Binding BorderColor, Converter={StaticResource EnumToBool}, ConverterParameter=#22c55e}"
|
||
Click="WebcamMenu_SetBorderColor"/>
|
||
<MenuItem Header="#eab308" Tag="#eab308" IsCheckable="True"
|
||
IsChecked="{Binding BorderColor, Converter={StaticResource EnumToBool}, ConverterParameter=#eab308}"
|
||
Click="WebcamMenu_SetBorderColor"/>
|
||
<MenuItem Header="#38bdf8" Tag="#38bdf8" IsCheckable="True"
|
||
IsChecked="{Binding BorderColor, Converter={StaticResource EnumToBool}, ConverterParameter=#38bdf8}"
|
||
Click="WebcamMenu_SetBorderColor"/>
|
||
</MenuItem>
|
||
<MenuItem Header="Border Effect">
|
||
<MenuItem Header="None" Tag="None" IsCheckable="True"
|
||
IsChecked="{Binding BorderAnimation, Converter={StaticResource EnumToBool}, ConverterParameter=None}"
|
||
Click="WebcamMenu_SetBorderAnimation"/>
|
||
<MenuItem Header="Pulse" Tag="Pulse" IsCheckable="True"
|
||
IsChecked="{Binding BorderAnimation, Converter={StaticResource EnumToBool}, ConverterParameter=Pulse}"
|
||
Click="WebcamMenu_SetBorderAnimation"/>
|
||
<MenuItem Header="Chase" Tag="Chase" IsCheckable="True"
|
||
IsChecked="{Binding BorderAnimation, Converter={StaticResource EnumToBool}, ConverterParameter=Chase}"
|
||
Click="WebcamMenu_SetBorderAnimation"/>
|
||
<MenuItem Header="Rainbow" Tag="Rainbow" IsCheckable="True"
|
||
IsChecked="{Binding BorderAnimation, Converter={StaticResource EnumToBool}, ConverterParameter=Rainbow}"
|
||
Click="WebcamMenu_SetBorderAnimation"/>
|
||
<MenuItem Header="Shimmer" Tag="Shimmer" IsCheckable="True"
|
||
IsChecked="{Binding BorderAnimation, Converter={StaticResource EnumToBool}, ConverterParameter=Shimmer}"
|
||
Click="WebcamMenu_SetBorderAnimation"/>
|
||
<MenuItem Header="Marching Ants" Tag="MarchingAnts" IsCheckable="True"
|
||
IsChecked="{Binding BorderAnimation, Converter={StaticResource EnumToBool}, ConverterParameter=MarchingAnts}"
|
||
Click="WebcamMenu_SetBorderAnimation"/>
|
||
<MenuItem Header="Glow" Tag="Glow" IsCheckable="True"
|
||
IsChecked="{Binding BorderAnimation, Converter={StaticResource EnumToBool}, ConverterParameter=Glow}"
|
||
Click="WebcamMenu_SetBorderAnimation"/>
|
||
<MenuItem Header="Electricity" Tag="Electricity" IsCheckable="True"
|
||
IsChecked="{Binding BorderAnimation, Converter={StaticResource EnumToBool}, ConverterParameter=Electricity}"
|
||
Click="WebcamMenu_SetBorderAnimation"/>
|
||
<MenuItem Header="Sparkles" Tag="Sparkles" IsCheckable="True"
|
||
IsChecked="{Binding BorderAnimation, Converter={StaticResource EnumToBool}, ConverterParameter=Sparkles}"
|
||
Click="WebcamMenu_SetBorderAnimation"/>
|
||
</MenuItem>
|
||
<Separator/>
|
||
<MenuItem Header="Hide in this scene" Click="WebcamMenu_HideInScene"/>
|
||
<MenuItem Header="Remove" Click="WebcamMenu_Remove"/>
|
||
</ContextMenu>
|
||
</Grid.ContextMenu>
|
||
</Grid>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
|
||
<Grid x:Name="SelectionOverlay" IsHitTestVisible="False"
|
||
Canvas.Left="{Binding SelectedElement.X}" Canvas.Top="{Binding SelectedElement.Y}"
|
||
Width="{Binding SelectedElement.Width}" Height="{Binding SelectedElement.Height}">
|
||
<Rectangle Stroke="#e94560" StrokeDashArray="4 3" StrokeThickness="3"/>
|
||
<Ellipse Width="26" Height="26" Fill="#e94560" Stroke="White" StrokeThickness="2"
|
||
HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,-13,-13"/>
|
||
</Grid>
|
||
|
||
<ItemsControl ItemsSource="{Binding DimRects}" IsHitTestVisible="False">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate>
|
||
<Canvas/>
|
||
</ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemContainerStyle>
|
||
<Style TargetType="ContentPresenter">
|
||
<Setter Property="Canvas.Left" Value="{Binding X}"/>
|
||
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
|
||
</Style>
|
||
</ItemsControl.ItemContainerStyle>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate>
|
||
<Rectangle Width="{Binding Width}" Height="{Binding Height}"
|
||
Fill="#000000" Opacity="0.55"/>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
|
||
<Rectangle IsHitTestVisible="False" Stroke="#e94560" StrokeThickness="3"
|
||
Canvas.Left="{Binding OutputRectX}" Canvas.Top="{Binding OutputRectY}"
|
||
Width="{Binding OutputRectWidth}" Height="{Binding OutputRectHeight}"
|
||
Visibility="{Binding IsOutputCropped, Converter={StaticResource BoolToVis}}"/>
|
||
|
||
<!-- Free-tier branding credit. The source is the SAME rendered
|
||
frame FramePump composites into the recording/stream
|
||
(BrandFlashPresenter → MainViewModel.BrandFlashFrame), so
|
||
the preview cannot drift from the broadcast. It replaces a
|
||
preview-only TextBlock that no viewer ever saw, and the
|
||
fade/spread/placement is the renderer's — there is no
|
||
storyboard here to keep in sync. -->
|
||
<Grid x:Name="BrandFlashElement" Width="1920" Height="1080"
|
||
Canvas.Left="0" Canvas.Top="0"
|
||
Background="Transparent" IsHitTestVisible="False"
|
||
Visibility="{Binding BrandFlashVisible, Converter={StaticResource BoolToVis}}">
|
||
<Image Source="{Binding BrandFlashImageSource}"
|
||
Stretch="Fill" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||
</Grid>
|
||
|
||
<!-- Alert announcement strip (TASK 47): the same global
|
||
1920px overlay the output compositor blits along the
|
||
top edge. Global resource — never a Source. Without it
|
||
the strip reached the stream but never the preview. -->
|
||
<Grid x:Name="AlertTickerElement" Width="1920" Canvas.Top="0"
|
||
Background="Transparent" IsHitTestVisible="False"
|
||
Visibility="{Binding AlertTickerVisible, Converter={StaticResource BoolToVis}}">
|
||
<Image Source="{Binding AlertTickerImageSource}"
|
||
Stretch="Fill" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||
</Grid>
|
||
<!-- Social bar: one centered horizontal row at the top or bottom of
|
||
the frame, green glow while enabled. Global resource — never a
|
||
Source, no Sources entry. Click toggles top ⇄ bottom. -->
|
||
<Grid x:Name="SocialBarElement" Width="1920" Canvas.Top="{Binding SocialBarTop}"
|
||
Background="Transparent"
|
||
Visibility="{Binding SocialBarVisible, Converter={StaticResource BoolToVis}}"
|
||
MouseLeftButtonDown="SocialBar_MouseLeftButtonDown"
|
||
Cursor="Hand">
|
||
<Grid.Effect>
|
||
<DropShadowEffect Color="{Binding SocialBarGlowBrush.Color}"
|
||
BlurRadius="18" ShadowDepth="0" Opacity="0.9"/>
|
||
</Grid.Effect>
|
||
<ItemsControl ItemsSource="{Binding Socials.Entries}"
|
||
HorizontalAlignment="Center" Margin="40,8"
|
||
IsHitTestVisible="False">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate>
|
||
<StackPanel Orientation="Horizontal"/>
|
||
</ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate>
|
||
<StackPanel Orientation="Horizontal" Margin="0,0,20,0">
|
||
<Path Data="{Binding LogoData}" Fill="White" Width="24" Height="24"
|
||
Stretch="Uniform" VerticalAlignment="Center"/>
|
||
<TextBlock Text="{Binding Handle}" Foreground="White"
|
||
FontSize="14" VerticalAlignment="Center"
|
||
Margin="8,0,0,0"/>
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
</Grid>
|
||
</Canvas>
|
||
</Grid>
|
||
</Viewbox>
|
||
<Border HorizontalAlignment="Left" VerticalAlignment="Top" Margin="8,8,0,0"
|
||
Background="#CCe94560" CornerRadius="4" Padding="8,3" IsHitTestVisible="False"
|
||
Visibility="{Binding WebcamError, Converter={StaticResource NotNullToVis}}">
|
||
<TextBlock Text="{Binding WebcamError}" Foreground="White" FontSize="12"
|
||
MaxWidth="520" TextTrimming="CharacterEllipsis"/>
|
||
</Border>
|
||
<TextBlock Text="Preview" Foreground="#333" FontSize="24"
|
||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||
Visibility="{Binding ShowPreviewPlaceholder, Converter={StaticResource BoolToVis}}"/>
|
||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
|
||
Visibility="{Binding ShowEmptySceneHint, Converter={StaticResource BoolToVis}}">
|
||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||
<TextBlock Text="🖥️" Foreground="White" FontSize="30" Margin="0,0,20,0"/>
|
||
<TextBlock Text="📷" Foreground="White" FontSize="30" Margin="0,0,20,0"/>
|
||
<TextBlock Text="🖼️" Foreground="White" FontSize="30" Margin="0,0,20,0"/>
|
||
<TextBlock Text="✏️" Foreground="White" FontSize="30"/>
|
||
</StackPanel>
|
||
<TextBlock Text="Make it yours!" Foreground="#e0e0e0" FontSize="18"
|
||
FontWeight="SemiBold" HorizontalAlignment="Center" Margin="0,18,0,0"/>
|
||
<TextBlock Text="Add layers — your webcam, your screen, images, and text — to craft a brand and identity that's unmistakably you."
|
||
Foreground="#a0a0b0" FontSize="13" Margin="0,8,0,0"
|
||
TextWrapping="Wrap" MaxWidth="440" TextAlignment="Center"
|
||
HorizontalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Grid>
|
||
|
||
<!-- ═══ RIGHT-EDGE PULL-OUTS (always visible) ═══
|
||
White "Stream Settings" + "YPP" tabs stacked on the preview's right
|
||
edge; each toggles its own drawer (the same animated-width pattern).
|
||
Only one drawer is open at a time — IsDrawerOpen exclusivity is
|
||
VM-enforced (opening one collapses the other). Any click outside
|
||
the rail collapses whichever is open (MainWindow code-behind). -->
|
||
<Grid Grid.Row="0">
|
||
<StackPanel x:Name="TextPullOutHost" Orientation="Horizontal"
|
||
HorizontalAlignment="Right" VerticalAlignment="Stretch">
|
||
<!-- The broadcast-metadata drawer -->
|
||
<Border Background="#16213e" ClipToBounds="True">
|
||
<Border.Style>
|
||
<Style TargetType="Border">
|
||
<Setter Property="Width" Value="0"/>
|
||
<Style.Triggers>
|
||
<DataTrigger Binding="{Binding BroadcastForm.IsDrawerOpen}" Value="True">
|
||
<DataTrigger.EnterActions>
|
||
<BeginStoryboard>
|
||
<Storyboard>
|
||
<DoubleAnimation Storyboard.TargetProperty="Width"
|
||
To="320" Duration="0:0:0.2">
|
||
<DoubleAnimation.EasingFunction>
|
||
<CubicEase EasingMode="EaseOut"/>
|
||
</DoubleAnimation.EasingFunction>
|
||
</DoubleAnimation>
|
||
</Storyboard>
|
||
</BeginStoryboard>
|
||
</DataTrigger.EnterActions>
|
||
<DataTrigger.ExitActions>
|
||
<BeginStoryboard>
|
||
<Storyboard>
|
||
<DoubleAnimation Storyboard.TargetProperty="Width"
|
||
To="0" Duration="0:0:0.2">
|
||
<DoubleAnimation.EasingFunction>
|
||
<CubicEase EasingMode="EaseOut"/>
|
||
</DoubleAnimation.EasingFunction>
|
||
</DoubleAnimation>
|
||
</Storyboard>
|
||
</BeginStoryboard>
|
||
</DataTrigger.ExitActions>
|
||
</DataTrigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
</Border.Style>
|
||
<!-- Fixed-width inner so text never reflows mid-animation -->
|
||
<Grid Width="320" Margin="14,10,14,12">
|
||
<Grid.RowDefinitions>
|
||
<RowDefinition Height="Auto"/>
|
||
<RowDefinition Height="*"/>
|
||
<RowDefinition Height="Auto"/>
|
||
</Grid.RowDefinitions>
|
||
<Grid Grid.Row="0" Margin="0,0,0,8">
|
||
<TextBlock Text="BROADCAST TEXT" Style="{StaticResource SectionHeader}"
|
||
HorizontalAlignment="Left"/>
|
||
<Button HorizontalAlignment="Right" VerticalAlignment="Top"
|
||
Background="Transparent" BorderThickness="0" Cursor="Hand"
|
||
Command="{Binding BroadcastForm.CloseDrawerCommand}"
|
||
ToolTip="Close">
|
||
<Button.Template>
|
||
<ControlTemplate TargetType="Button">
|
||
<ContentPresenter/>
|
||
</ControlTemplate>
|
||
</Button.Template>
|
||
<TextBlock Text="✕" Foreground="#a0a0b0" FontSize="14" FontWeight="Bold"/>
|
||
</Button>
|
||
</Grid>
|
||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||
<StackPanel>
|
||
<TextBlock Text="TITLE" Style="{StaticResource YtLabel}" Margin="0,0,0,3"/>
|
||
<TextBox Style="{StaticResource YtTextBox}" Margin="0,0,0,10"
|
||
Text="{Binding BroadcastForm.Title, UpdateSourceTrigger=PropertyChanged}"/>
|
||
<TextBlock Text="DESCRIPTION" Style="{StaticResource YtLabel}" Margin="0,0,0,3"/>
|
||
<TextBox Style="{StaticResource YtTextBox}" Height="90" AcceptsReturn="True"
|
||
TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" Margin="0,0,0,10"
|
||
Text="{Binding BroadcastForm.Description, UpdateSourceTrigger=PropertyChanged}"/>
|
||
<TextBlock Text="TAGS (COMMA-SEPARATED)" Style="{StaticResource YtLabel}" Margin="0,0,0,3"/>
|
||
<TextBox Style="{StaticResource YtTextBox}" Margin="0,0,0,10"
|
||
Text="{Binding BroadcastForm.TagsCsv, UpdateSourceTrigger=PropertyChanged}"/>
|
||
<TextBlock Text="VISIBILITY" Style="{StaticResource YtLabel}" Margin="0,0,0,3"/>
|
||
<ComboBox Style="{StaticResource YtComboBox}" Margin="0,0,0,10"
|
||
ItemsSource="{Binding BroadcastForm.VisibilityOptions}"
|
||
SelectedItem="{Binding BroadcastForm.Visibility}"/>
|
||
<CheckBox Content="Made for kids" Foreground="#e0e0e0" FontSize="12"
|
||
Margin="0,2,0,10"
|
||
IsChecked="{Binding BroadcastForm.MadeForKids}"/>
|
||
<TextBlock Text="SCHEDULED START" Style="{StaticResource YtLabel}" Margin="0,0,0,3"/>
|
||
<TextBlock Text="{Binding BroadcastForm.ScheduledStartText}"
|
||
Foreground="#888899" FontSize="12" Margin="0,0,0,4"/>
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
<StackPanel Grid.Row="2" Margin="0,10,0,0">
|
||
<Button Style="{StaticResource YtButton}" Content="Update Broadcast"
|
||
Command="{Binding BroadcastForm.UpdateBroadcastCommand}"/>
|
||
<TextBlock Text="{Binding BroadcastForm.UpdateError}"
|
||
Foreground="#ef4444" FontSize="11" TextWrapping="Wrap" Margin="0,6,0,0"
|
||
Visibility="{Binding BroadcastForm.UpdateError, Converter={StaticResource NotNullToVis}}"/>
|
||
</StackPanel>
|
||
</Grid>
|
||
</Border>
|
||
<!-- The YPP-progress drawer (below Stream Settings on the rail) -->
|
||
<Border Background="#16213e" ClipToBounds="True">
|
||
<Border.Style>
|
||
<Style TargetType="Border">
|
||
<Setter Property="Width" Value="0"/>
|
||
<Style.Triggers>
|
||
<DataTrigger Binding="{Binding Ypp.IsDrawerOpen}" Value="True">
|
||
<DataTrigger.EnterActions>
|
||
<BeginStoryboard>
|
||
<Storyboard>
|
||
<DoubleAnimation Storyboard.TargetProperty="Width"
|
||
To="320" Duration="0:0:0.2">
|
||
<DoubleAnimation.EasingFunction>
|
||
<CubicEase EasingMode="EaseOut"/>
|
||
</DoubleAnimation.EasingFunction>
|
||
</DoubleAnimation>
|
||
</Storyboard>
|
||
</BeginStoryboard>
|
||
</DataTrigger.EnterActions>
|
||
<DataTrigger.ExitActions>
|
||
<BeginStoryboard>
|
||
<Storyboard>
|
||
<DoubleAnimation Storyboard.TargetProperty="Width"
|
||
To="0" Duration="0:0:0.2">
|
||
<DoubleAnimation.EasingFunction>
|
||
<CubicEase EasingMode="EaseOut"/>
|
||
</DoubleAnimation.EasingFunction>
|
||
</DoubleAnimation>
|
||
</Storyboard>
|
||
</BeginStoryboard>
|
||
</DataTrigger.ExitActions>
|
||
</DataTrigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
</Border.Style>
|
||
<Grid Width="320" Margin="14,10,14,12">
|
||
<Grid.RowDefinitions>
|
||
<RowDefinition Height="Auto"/>
|
||
<RowDefinition Height="*"/>
|
||
<RowDefinition Height="Auto"/>
|
||
</Grid.RowDefinitions>
|
||
<Grid Grid.Row="0" Margin="0,0,0,8">
|
||
<TextBlock Text="YPP PROGRESS" Style="{StaticResource SectionHeader}"
|
||
HorizontalAlignment="Left"/>
|
||
<Button HorizontalAlignment="Right" VerticalAlignment="Top"
|
||
Background="Transparent" BorderThickness="0" Cursor="Hand"
|
||
Command="{Binding Ypp.CloseDrawerCommand}"
|
||
ToolTip="Close">
|
||
<Button.Template>
|
||
<ControlTemplate TargetType="Button">
|
||
<ContentPresenter/>
|
||
</ControlTemplate>
|
||
</Button.Template>
|
||
<TextBlock Text="✕" Foreground="#a0a0b0" FontSize="14" FontWeight="Bold"/>
|
||
</Button>
|
||
</Grid>
|
||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||
<StackPanel>
|
||
<TextBlock Text="Sign in to YouTube to see your Partner Program progress and eligibility checklist here."
|
||
Foreground="#d0d0e0" FontSize="13" TextWrapping="Wrap"
|
||
Visibility="{Binding Ypp.IsSignedOut, Converter={StaticResource BoolToVis}}"/>
|
||
<TextBlock Text="Refreshing from YouTube…" Foreground="#a0a0b0" FontSize="12"
|
||
Margin="0,0,0,4"
|
||
Visibility="{Binding Ypp.IsLoading, Converter={StaticResource BoolToVis}}"/>
|
||
<TextBlock Text="{Binding Ypp.LoadError}" Foreground="#ef4444" FontSize="11"
|
||
TextWrapping="Wrap" Margin="0,0,0,4"
|
||
Visibility="{Binding Ypp.LoadError, Converter={StaticResource NotNullToVis}}"/>
|
||
<StackPanel Visibility="{Binding Ypp.IsSignedOut, Converter={StaticResource InverseBoolToVis}}">
|
||
<TextBlock Text="{Binding Ypp.DisplayName}" Foreground="#e0e0e0" FontSize="13"
|
||
FontWeight="SemiBold" Margin="0,0,0,10"/>
|
||
<TextBlock Text="SUBSCRIBERS — TIER 1 · FAN FUNDING" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
|
||
<TextBlock Text="{Binding Ypp.Tier1SubsText}" Foreground="#e0e0e0" FontSize="13"/>
|
||
<Border Height="8" Background="#2a2a40" CornerRadius="4" Margin="0,4,0,10">
|
||
<Border Height="8" Background="#e94560" CornerRadius="4" HorizontalAlignment="Left"
|
||
Width="{Binding Ypp.Tier1SubsBarWidth}"/>
|
||
</Border>
|
||
<TextBlock Text="SUBSCRIBERS — TIER 2 · AD REVENUE" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
|
||
<TextBlock Text="{Binding Ypp.Tier2SubsText}" Foreground="#e0e0e0" FontSize="13"/>
|
||
<Border Height="8" Background="#2a2a40" CornerRadius="4" Margin="0,4,0,10">
|
||
<Border Height="8" Background="#e94560" CornerRadius="4" HorizontalAlignment="Left"
|
||
Width="{Binding Ypp.Tier2SubsBarWidth}"/>
|
||
</Border>
|
||
<TextBlock Text="PUBLIC UPLOADS (90 DAYS)" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
|
||
<TextBlock Text="{Binding Ypp.UploadsText}" Foreground="#e0e0e0" FontSize="13"/>
|
||
<Border Height="8" Background="#2a2a40" CornerRadius="4" Margin="0,4,0,6">
|
||
<Border Height="8" Background="#e94560" CornerRadius="4" HorizontalAlignment="Left"
|
||
Width="{Binding Ypp.UploadsBarWidth}"/>
|
||
</Border>
|
||
<TextBlock Text="LONG-FORM WATCH HOURS + SHORTS VIEWS" Style="{StaticResource YtLabel}" Margin="0,10,0,2"/>
|
||
<TextBlock Text="These arrive with the extended analytics update — subscriber + upload + standing progress is live now."
|
||
Foreground="#666677" FontSize="12" TextWrapping="Wrap"/>
|
||
|
||
<TextBlock Text="COMPLIANCE CHECKLIST" Style="{StaticResource YtLabel}" Margin="0,14,0,4"/>
|
||
<TextBlock Text="CHANNEL STANDING" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
|
||
<TextBlock Text="{Binding Ypp.StandingDetailText}" Foreground="#888899" FontSize="11"
|
||
TextWrapping="Wrap" Margin="0,0,0,8"/>
|
||
<CheckBox Content="Two-Step Verification switched on" Foreground="#e0e0e0" FontSize="12"
|
||
Margin="0,2,0,0"
|
||
IsChecked="{Binding Ypp.TwoFactorEnabled}"/>
|
||
<Button Background="Transparent" BorderThickness="0" Cursor="Hand" HorizontalAlignment="Left"
|
||
Command="{Binding Ypp.OpenTwoStepCommand}" ToolTip="Open Google's 2-Step Verification page">
|
||
<Button.Template>
|
||
<ControlTemplate TargetType="Button">
|
||
<ContentPresenter/>
|
||
</ControlTemplate>
|
||
</Button.Template>
|
||
<TextBlock Text="Set up on Google →" Foreground="#5aa7ff" FontSize="12" Margin="0,0,0,6"/>
|
||
</Button>
|
||
<CheckBox Content="AdSense account linked" Foreground="#e0e0e0" FontSize="12"
|
||
Margin="0,2,0,0"
|
||
IsChecked="{Binding Ypp.AdSenseLinked}"/>
|
||
<Button Background="Transparent" BorderThickness="0" Cursor="Hand" HorizontalAlignment="Left"
|
||
Command="{Binding Ypp.OpenEarnCommand}" ToolTip="Open the YouTube Earn page">
|
||
<Button.Template>
|
||
<ControlTemplate TargetType="Button">
|
||
<ContentPresenter/>
|
||
</ControlTemplate>
|
||
</Button.Template>
|
||
<TextBlock Text="How to link on YouTube →" Foreground="#5aa7ff" FontSize="12" Margin="0,0,0,6"/>
|
||
</Button>
|
||
|
||
<TextBlock Text="WHAT COUNTS" Style="{StaticResource YtLabel}" Margin="0,10,0,4"/>
|
||
<TextBlock Text="{Binding Ypp.WhatCountsText}" Foreground="#a0a0b0" FontSize="12"
|
||
TextWrapping="Wrap" Margin="0,0,0,6"/>
|
||
</StackPanel>
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
<StackPanel Grid.Row="2" Margin="0,10,0,0">
|
||
<Button Style="{StaticResource YtButton}" Content="Refresh from YouTube"
|
||
Command="{Binding Ypp.RefreshCommand}"/>
|
||
<TextBlock Text="{Binding Ypp.LastUpdatedText}" Foreground="#888899" FontSize="11" Margin="0,6,0,0"/>
|
||
</StackPanel>
|
||
</Grid>
|
||
</Border>
|
||
<!-- The Test-stream drawer (below YPP on the rail) -->
|
||
<Border Background="#16213e" ClipToBounds="True">
|
||
<Border.Style>
|
||
<Style TargetType="Border">
|
||
<Setter Property="Width" Value="0"/>
|
||
<Style.Triggers>
|
||
<DataTrigger Binding="{Binding TestSession.IsDrawerOpen}" Value="True">
|
||
<DataTrigger.EnterActions>
|
||
<BeginStoryboard>
|
||
<Storyboard>
|
||
<DoubleAnimation Storyboard.TargetProperty="Width"
|
||
To="320" Duration="0:0:0.2">
|
||
<DoubleAnimation.EasingFunction>
|
||
<CubicEase EasingMode="EaseOut"/>
|
||
</DoubleAnimation.EasingFunction>
|
||
</DoubleAnimation>
|
||
</Storyboard>
|
||
</BeginStoryboard>
|
||
</DataTrigger.EnterActions>
|
||
<DataTrigger.ExitActions>
|
||
<BeginStoryboard>
|
||
<Storyboard>
|
||
<DoubleAnimation Storyboard.TargetProperty="Width"
|
||
To="0" Duration="0:0:0.2">
|
||
<DoubleAnimation.EasingFunction>
|
||
<CubicEase EasingMode="EaseOut"/>
|
||
</DoubleAnimation.EasingFunction>
|
||
</DoubleAnimation>
|
||
</Storyboard>
|
||
</BeginStoryboard>
|
||
</DataTrigger.ExitActions>
|
||
</DataTrigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
</Border.Style>
|
||
<Grid Width="320" Margin="14,10,14,12">
|
||
<Grid.RowDefinitions>
|
||
<RowDefinition Height="Auto"/>
|
||
<RowDefinition Height="*"/>
|
||
<RowDefinition Height="Auto"/>
|
||
</Grid.RowDefinitions>
|
||
<Grid Grid.Row="0" Margin="0,0,0,8">
|
||
<TextBlock Text="TEST STREAM" Style="{StaticResource SectionHeader}"
|
||
HorizontalAlignment="Left"/>
|
||
<Button HorizontalAlignment="Right" VerticalAlignment="Top"
|
||
Background="Transparent" BorderThickness="0" Cursor="Hand"
|
||
Command="{Binding TestSession.CloseDrawerCommand}"
|
||
ToolTip="Close">
|
||
<Button.Template>
|
||
<ControlTemplate TargetType="Button">
|
||
<ContentPresenter/>
|
||
</ControlTemplate>
|
||
</Button.Template>
|
||
<TextBlock Text="✕" Foreground="#a0a0b0" FontSize="14" FontWeight="Bold"/>
|
||
</Button>
|
||
</Grid>
|
||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||
<StackPanel>
|
||
<TextBlock Text="{Binding TestSession.SessionCaption}"
|
||
Foreground="#d0d0e0" FontSize="13" TextWrapping="Wrap" Margin="0,0,0,8"/>
|
||
<TextBlock Text="{Binding TestSession.ChatIdLine}"
|
||
Foreground="#888899" FontSize="11"
|
||
Visibility="{Binding TestSession.IsTestSession, Converter={StaticResource BoolToVis}}"/>
|
||
|
||
<TextBlock Text="MOCK CHAT INPUT" Style="{StaticResource YtLabel}" Margin="0,10,0,3"/>
|
||
<TextBlock Text="Sends a real live chat message — it round-trips through the actual chat poll and renders on your chat overlay."
|
||
Foreground="#a0a0b0" FontSize="11" TextWrapping="Wrap" Margin="0,0,0,4"/>
|
||
<TextBox Style="{StaticResource YtTextBox}" Margin="0,0,0,6"
|
||
Text="{Binding TestSession.SendMessageText, UpdateSourceTrigger=PropertyChanged}"/>
|
||
<Button Style="{StaticResource YtButton}" Content="Send Message"
|
||
Command="{Binding TestSession.SendTestMessageCommand}"/>
|
||
|
||
<TextBlock Text="SIMULATED EVENTS" Style="{StaticResource YtLabel}" Margin="0,14,0,3"/>
|
||
<TextBlock Text="Fired into your chat overlay locally — YouTube's insert API only creates text, so these never reach YouTube."
|
||
Foreground="#a0a0b0" FontSize="11" TextWrapping="Wrap" Margin="0,0,0,4"/>
|
||
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||
<Button Style="{StaticResource YtButtonSecondary}" Content="Subscriber" Padding="10,4" FontSize="11"
|
||
Margin="0,0,6,0" Command="{Binding TestSession.SimulateSubscriberCommand}"/>
|
||
<Button Style="{StaticResource YtButtonSecondary}" Content="New Member" Padding="10,4" FontSize="11"
|
||
Margin="0,0,6,0" Command="{Binding TestSession.SimulateMemberCommand}"/>
|
||
</StackPanel>
|
||
<Button Style="{StaticResource YtButtonSecondary}" Content="Super Chat (5.00)" Padding="10,4" FontSize="11"
|
||
HorizontalAlignment="Left" Command="{Binding TestSession.SimulateSuperChatCommand}"/>
|
||
|
||
<TextBlock Text="{Binding TestSession.LastEventText}"
|
||
Foreground="#22c55e" FontSize="11" TextWrapping="Wrap" Margin="0,10,0,0"
|
||
Visibility="{Binding TestSession.LastEventText, Converter={StaticResource NotNullToVis}}"/>
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
<StackPanel Grid.Row="2" Margin="0,10,0,0">
|
||
<Button Style="{StaticResource YtButton}" Content="End Test"
|
||
Command="{Binding TestSession.EndTestCommand}"
|
||
Visibility="{Binding TestSession.IsTestSession, Converter={StaticResource BoolToVis}}"/>
|
||
<TextBlock Text="Test broadcasts are private — nobody is notified, and it runs through the exact same pipeline as a real live."
|
||
Foreground="#666677" FontSize="11" TextWrapping="Wrap" Margin="0,6,0,0"/>
|
||
</StackPanel>
|
||
</Grid>
|
||
</Border>
|
||
<!-- The stacked white tabs -->
|
||
<StackPanel Orientation="Vertical" HorizontalAlignment="Right" VerticalAlignment="Stretch">
|
||
<Border Width="30" VerticalAlignment="Center" Background="#ffffff"
|
||
CornerRadius="4,0,0,4" Padding="0,10" Cursor="Hand" Margin="0,4,0,0"
|
||
ToolTip="Broadcast text fields">
|
||
<Border.InputBindings>
|
||
<MouseBinding Gesture="LeftClick"
|
||
Command="{Binding BroadcastForm.ToggleDrawerCommand}"/>
|
||
</Border.InputBindings>
|
||
<TextBlock Text="Stream Settings" Foreground="#e94560" FontWeight="Bold" FontSize="15"
|
||
HorizontalAlignment="Center">
|
||
<TextBlock.LayoutTransform>
|
||
<RotateTransform Angle="90"/>
|
||
</TextBlock.LayoutTransform>
|
||
</TextBlock>
|
||
</Border>
|
||
<Border Width="30" VerticalAlignment="Center" Background="#ffffff"
|
||
CornerRadius="4,0,0,4" Padding="0,10" Cursor="Hand" Margin="0,4,0,0"
|
||
ToolTip="YouTube Partner Program progress">
|
||
<Border.InputBindings>
|
||
<MouseBinding Gesture="LeftClick"
|
||
Command="{Binding Ypp.ToggleDrawerCommand}"/>
|
||
</Border.InputBindings>
|
||
<TextBlock Text="YPP" Foreground="#e94560" FontWeight="Bold" FontSize="15"
|
||
HorizontalAlignment="Center">
|
||
<TextBlock.LayoutTransform>
|
||
<RotateTransform Angle="90"/>
|
||
</TextBlock.LayoutTransform>
|
||
</TextBlock>
|
||
</Border>
|
||
<Border Width="30" VerticalAlignment="Center" Background="#ffffff"
|
||
CornerRadius="4,0,0,4" Padding="0,10" Cursor="Hand" Margin="0,4,0,0"
|
||
ToolTip="Test stream — mock chat input and simulated events">
|
||
<Border.InputBindings>
|
||
<MouseBinding Gesture="LeftClick"
|
||
Command="{Binding TestSession.ToggleDrawerCommand}"/>
|
||
</Border.InputBindings>
|
||
<TextBlock Text="TEST" Foreground="#9c6f1c" FontWeight="Bold" FontSize="15"
|
||
HorizontalAlignment="Center">
|
||
<TextBlock.LayoutTransform>
|
||
<RotateTransform Angle="90"/>
|
||
</TextBlock.LayoutTransform>
|
||
</TextBlock>
|
||
</Border>
|
||
</StackPanel>
|
||
</StackPanel>
|
||
</Grid>
|
||
|
||
<!-- ═══ LIVE CONTROLS (below preview, never overlaps) ═══
|
||
Line 1: Socials + TRAX meter (desktop/game audio)
|
||
Line 2: Mic meter + volume -->
|
||
<StackPanel Grid.Row="2" Orientation="Vertical" HorizontalAlignment="Center"
|
||
VerticalAlignment="Center" Margin="0,4">
|
||
<!-- Line 1: Socials + TRAX -->
|
||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,4">
|
||
<!-- Socials button: hover glows the social bar in the preview -->
|
||
<Button x:Name="SocialsButton"
|
||
Command="{Binding OpenSocialDialogCommand}"
|
||
Style="{StaticResource YtButtonSecondary}" Padding="12,4"
|
||
FontSize="11" VerticalAlignment="Center" Margin="0,0,12,0"
|
||
ToolTip="Add or manage your social handles"
|
||
MouseEnter="SocialsButton_MouseEnter"
|
||
MouseLeave="SocialsButton_MouseLeave">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Ellipse Width="8" Height="8" Fill="{Binding SocialBarDotBrush}"
|
||
Margin="0,0,6,0" VerticalAlignment="Center"/>
|
||
<TextBlock Text="Socials"/>
|
||
</StackPanel>
|
||
</Button>
|
||
<!-- TRAX meter -->
|
||
<Border Background="#D9000000" CornerRadius="6" Padding="12,8">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Button Style="{StaticResource YtButtonSecondary}" Padding="8,2"
|
||
FontSize="11" VerticalAlignment="Center" Margin="0,0,8,0"
|
||
ToolTipService.ToolTip="{Binding TraxToolTip}"
|
||
ToolTipService.Placement="Top"
|
||
Click="TraxButton_Click"
|
||
PreviewMouseRightButtonUp="TraxButton_PreviewMouseRightButtonUp">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Ellipse Width="8" Height="8" Fill="{Binding TraxDotBrush}"
|
||
Margin="0,0,6,0" VerticalAlignment="Center"/>
|
||
<TextBlock Text="TRAX"/>
|
||
</StackPanel>
|
||
</Button>
|
||
<TextBlock Text="Desktop Audio" Foreground="#a0a0b0" FontSize="11"
|
||
FontWeight="SemiBold" VerticalAlignment="Center" Margin="0,0,8,0"
|
||
ToolTip="Desktop/game audio via WASAPI loopback (automatic); TRAX music rides the same channel"/>
|
||
<Border Width="288" Height="14" CornerRadius="7" Background="#3a3b52" Margin="0,0,8,0"
|
||
ClipToBounds="True" VerticalAlignment="Center">
|
||
<Grid>
|
||
<Rectangle Width="57" Fill="#4a4520" HorizontalAlignment="Left" Margin="173,0,0,0"
|
||
VerticalAlignment="Stretch"/>
|
||
<Rectangle Width="58" Fill="#4a2222" HorizontalAlignment="Left" Margin="230,0,0,0"
|
||
VerticalAlignment="Stretch"/>
|
||
<Border HorizontalAlignment="Left" Width="{Binding GameMeterFillWidth}"
|
||
Background="{Binding GameMeterBrush}" Opacity="0.75"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="36,0,0,0"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="72,0,0,0"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="108,0,0,0"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="144,0,0,0"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="180,0,0,0"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="216,0,0,0"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="252,0,0,0"/>
|
||
<Rectangle Width="2" Fill="#00000080" HorizontalAlignment="Left" Margin="173,0,0,0"
|
||
VerticalAlignment="Stretch"/>
|
||
<Rectangle Width="2" Fill="#00000080" HorizontalAlignment="Left" Margin="230,0,0,0"
|
||
VerticalAlignment="Stretch"/>
|
||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||
Visibility="{Binding TraxError, Converter={StaticResource NotNullToVis}}"
|
||
VerticalAlignment="Center" Margin="4,0">
|
||
<TextBlock Text="{Binding TraxError}" Foreground="#e94560" FontSize="9"
|
||
FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||
</ScrollViewer>
|
||
</Grid>
|
||
</Border>
|
||
<Grid Width="16" Height="16" Cursor="Hand" Margin="0,0,8,0"
|
||
VerticalAlignment="Center" ToolTip="{Binding GameMuteText}"
|
||
MouseLeftButtonUp="GameSpeaker_MouseLeftButtonUp">
|
||
<Path Fill="#d0d0d0" Stretch="Uniform"
|
||
Data="M3,9v6h4l5,5V4L7,9H3zM16.5,12c0,-1.77 -1,-3.29 -2.5,-4.03v8.05c1.5,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"
|
||
Visibility="{Binding GameMuted, Converter={StaticResource InverseBoolToVis}}"/>
|
||
<Path Fill="#ef4444" Stretch="Uniform"
|
||
Data="M3,9v6h4l5,5V4L7,9H3zM16.5,12c0,-1.77 -1,-3.29 -2.5,-4.03v8.05c1.5,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"
|
||
Visibility="{Binding GameMuted, Converter={StaticResource BoolToVis}}"/>
|
||
<Path Stroke="#ef4444" StrokeThickness="2.5" StrokeEndLineCap="Round" Stretch="Uniform"
|
||
Data="M21,3 L3,21"
|
||
Visibility="{Binding GameMuted, Converter={StaticResource BoolToVis}}"/>
|
||
</Grid>
|
||
<Slider Width="130" Minimum="0" Maximum="1"
|
||
Value="{Binding GameAudioVolume, Mode=TwoWay}" VerticalAlignment="Center"
|
||
PreviewMouseLeftButtonDown="GameVolumeSlider_PreviewMouseLeftButtonDown"
|
||
PreviewMouseLeftButtonUp="GameVolumeSlider_PreviewMouseLeftButtonUp"
|
||
LostMouseCapture="GameVolumeSlider_LostMouseCapture"/>
|
||
</StackPanel>
|
||
</Border>
|
||
</StackPanel>
|
||
<!-- Line 2: Mic meter + volume -->
|
||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||
<Button Command="{Binding OpenMicPickerCommand}"
|
||
Style="{StaticResource YtButtonSecondary}" Padding="12,4"
|
||
FontSize="11" VerticalAlignment="Center" Margin="0,0,8,0"
|
||
ToolTip="{Binding MicStatusToolTip}">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Ellipse Width="8" Height="8" Fill="{Binding MicStatusBrush}"
|
||
Margin="0,0,6,0" VerticalAlignment="Center"/>
|
||
<TextBlock Text="MIC"/>
|
||
</StackPanel>
|
||
</Button>
|
||
<Border Width="288" Height="14" CornerRadius="7" Background="#3a3b52" Margin="0,0,8,0"
|
||
ClipToBounds="True" VerticalAlignment="Center">
|
||
<Grid>
|
||
<Rectangle Width="57" Fill="#4a4520" HorizontalAlignment="Left" Margin="173,0,0,0"
|
||
VerticalAlignment="Stretch"/>
|
||
<Rectangle Width="58" Fill="#4a2222" HorizontalAlignment="Left" Margin="230,0,0,0"
|
||
VerticalAlignment="Stretch"/>
|
||
<TextBlock Text="{Binding MicSourceName}" Foreground="#b8b8c8" FontSize="10"
|
||
VerticalAlignment="Center" Margin="6,0,0,0" MaxWidth="270"
|
||
TextTrimming="CharacterEllipsis"
|
||
Visibility="{Binding MicSourceName, Converter={StaticResource NotNullToVis}}"/>
|
||
<Border HorizontalAlignment="Left" Width="{Binding MeterFillWidth}"
|
||
Background="{Binding MeterBrush}" Opacity="0.75"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="36,0,0,0"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="72,0,0,0"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="108,0,0,0"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="144,0,0,0"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="180,0,0,0"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="216,0,0,0"/>
|
||
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="252,0,0,0"/>
|
||
<Rectangle Width="2" Fill="#00000080" HorizontalAlignment="Left" Margin="173,0,0,0"
|
||
VerticalAlignment="Stretch"/>
|
||
<Rectangle Width="2" Fill="#00000080" HorizontalAlignment="Left" Margin="230,0,0,0"
|
||
VerticalAlignment="Stretch"/>
|
||
</Grid>
|
||
</Border>
|
||
<Grid Width="16" Height="16" Cursor="Hand" Margin="0,0,8,0"
|
||
VerticalAlignment="Center" ToolTip="{Binding MicMuteText}"
|
||
MouseLeftButtonUp="MicSpeaker_MouseLeftButtonUp">
|
||
<Path Fill="#d0d0d0" Stretch="Uniform"
|
||
Data="M12,14c1.66,0 3,-1.34 3,-3V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6C9,12.66 10.34,14 12,14zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.49 6,-3.31 6,-6.72H17.3z"
|
||
Visibility="{Binding MicMuted, Converter={StaticResource InverseBoolToVis}}"/>
|
||
<Path Fill="#ef4444" Stretch="Uniform"
|
||
Data="M12,14c1.66,0 3,-1.34 3,-3V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6C9,12.66 10.34,14 12,14zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.49 6,-3.31 6,-6.72H17.3z"
|
||
Visibility="{Binding MicMuted, Converter={StaticResource BoolToVis}}"/>
|
||
<Path Stroke="#ef4444" StrokeThickness="2.5" StrokeEndLineCap="Round" Stretch="Uniform"
|
||
Data="M21,3 L3,21"
|
||
Visibility="{Binding MicMuted, Converter={StaticResource BoolToVis}}"/>
|
||
</Grid>
|
||
<Slider Width="130" Minimum="0" Maximum="1"
|
||
Value="{Binding MicVolume, Mode=TwoWay}" VerticalAlignment="Center"
|
||
PreviewMouseLeftButtonDown="VolumeSlider_PreviewMouseLeftButtonDown"
|
||
PreviewMouseLeftButtonUp="VolumeSlider_PreviewMouseLeftButtonUp"
|
||
LostMouseCapture="VolumeSlider_LostMouseCapture"/>
|
||
<StackPanel Orientation="Horizontal" Margin="10,0,0,0" VerticalAlignment="Center"
|
||
ToolTip="Audio sync (−500..+500 ms). Positive: delays the audio when it runs ahead of the video. Negative: advances it (drops the first |N| ms of the recording) when it runs behind. Calibrate with a clap: is the clap late in the video, move negative; early, move positive.">
|
||
<TextBlock Text="AUDIO SYNC" FontSize="9" Foreground="#9898aa"
|
||
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||
<Slider Width="70" Minimum="-500" Maximum="500"
|
||
Value="{Binding AudioSyncOffsetMs, Mode=TwoWay}"
|
||
IsEnabled="{Binding IsEditMode}"
|
||
VerticalAlignment="Center"
|
||
ToolTip="{Binding AudioSyncOffsetMs, StringFormat=Audio sync: {0} ms}"/>
|
||
</StackPanel>
|
||
</StackPanel>
|
||
</StackPanel>
|
||
</Grid>
|
||
</Border>
|
||
</UserControl>
|