7f14ffb11e
The creator confirmed the clip fix ("the video plays now"), then asked where the
scrolling text was — it was invisible, for a structural reason. AlertTickerFrame
existed only as a frame-pump callback blitted into the OUTPUT; PreviewPane.xaml had
no element for it, because the strip is master-width and global, not a Source, so it
cannot ride a per-element Image. Nothing was wrong in the renderer: there was no
consumer in the preview. Same class of defect as the missing IsAlertBox trigger, one
layer up (MyMistakes RULE 5/6).
- tickerPreviewSink on AlertOverlayLayer, published from RefreshAlertPreviews() so it
is always the UI thread; MainViewModel.AlertTicker writes it into one reused
WriteableBitmap bound to a new global AlertTickerElement, mirroring SocialBarElement.
- Source.AlertDisplayMethod + panel "Display" selector: TickerScroll / Flash / Solid.
Flash pulses 0.5s on / 0.5s off for the whole alert; Solid is centred and still.
- The marquee was also unreadable: a fixed 140px/s took ~17s per pass, so a 10s alert
showed the text once, entering from the right and never crossing. Paced in reads per
alert instead (TickerReadsPerAlert = 3 inside the alert's own length, speed derived
from it) — never px/s. Research (websearch: how do OBS/Streamlabs/StreamElements
alert boxes present announcement timing?) settled the unit: Streamlabs exposes "Alert
Duration: choose how long your alert stays on your stream" and "Text Delay", never a
scroll-speed slider (https://support.streamlabs.com/hc/en-us/articles/52499995174299-Setting-up-Your-Streamlabs-Alerts).
Run is phase-started half a frame in so the first frame isn't blank.
- Persistence: AlertDisplayMethod INTEGER NOT NULL DEFAULT 0 via the idempotent
table_info migration, appended LAST in the SELECT because the Source reader is
positional (GetInt32(32..34)) — a mid-list insert would silently shift a neighbour.
Tests: 15 new facts (suite 339/339). RealApp STA host: the pane draws the strip and
collapses at alert end; the layer publishes a real 1920x48 frame for all three methods
and nothing when the ticker is off; three passes counted in 10s by the pill's leading
edge resetting (a seamless marquee never blanks, so an empty frame cannot count a pass);
Solid byte-identical at every moment; Flash on for half of each second; the panel shows
and writes back the choice; the DB round-trips all three alert fields together.
Incidental finding: a bound ItemsSource ComboBox in LeftPanel.xaml broke
LayerReorderPersistenceTests.RealMouseDrag (that test injects PHYSICAL mouse input, so a
load-time re-measure moves the rows out from under the cursor). Rewritten as inline
ComboBoxItems, the shape the chat Font selector already uses in that panel. Recorded as
MyMistakes RULE (8).
608 lines
50 KiB
XML
608 lines
50 KiB
XML
<UserControl x:Class="ytLive.Controls.LeftPanel"
|
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
xmlns:Helpers="clr-namespace:ytLive.Helpers"
|
|
xmlns:models="clr-namespace:ytLive.Models">
|
|
<Border Background="#16213e" CornerRadius="6" Padding="12">
|
|
<Grid>
|
|
<Grid.RowDefinitions>
|
|
<RowDefinition Height="*"/>
|
|
</Grid.RowDefinitions>
|
|
|
|
<!-- Edit mode (offline): layers + properties -->
|
|
<Grid Grid.Row="0" Visibility="{Binding IsEditMode, Converter={StaticResource BoolToVis}}">
|
|
<Grid.RowDefinitions>
|
|
<RowDefinition Height="Auto"/>
|
|
<RowDefinition Height="Auto"/>
|
|
<RowDefinition Height="Auto"/>
|
|
<RowDefinition Height="Auto"/>
|
|
<RowDefinition Height="Auto"/>
|
|
<RowDefinition Height="Auto"/>
|
|
<RowDefinition Height="*"/>
|
|
</Grid.RowDefinitions>
|
|
|
|
<!-- LAYERS header + Add button -->
|
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,8,0,0">
|
|
<TextBlock Text="LAYERS" Style="{StaticResource SectionHeader}" VerticalAlignment="Center"/>
|
|
<Button ToolTip="Add Layer" Style="{StaticResource IconButton}" Margin="4,-4,0,0"
|
|
Click="AddSourceButton_Click">
|
|
<Button.ContextMenu>
|
|
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
|
<MenuItem Header="Webcam" Command="{Binding AddWebcamCommand}"
|
|
IsEnabled="{Binding CanAddWebcam}"
|
|
ToolTip="{Binding WebcamAddToolTip}"/>
|
|
<MenuItem Header="Image" Command="{Binding AddImageCommand}"/>
|
|
<MenuItem Header="Text" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.TextOverlay}"/>
|
|
<MenuItem Header="Countdown" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.Countdown}"
|
|
Visibility="{Binding IsCountdownAvailable, Converter={StaticResource BoolToVis}}"
|
|
ToolTip="Timer overlay — Starting and BRB screens only"/>
|
|
<MenuItem Header="Web Resource" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.WebSource}"
|
|
ToolTip="Embedded web page — set URI in Properties panel"/>
|
|
<MenuItem Header="YouTube Chat" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.ChatBox}"
|
|
IsEnabled="{Binding CanAddYouTubeChat}"
|
|
ToolTip="One chat layer at a time — it's already in your stream"/>
|
|
<MenuItem Header="Stream Alerts" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.AlertBox}"
|
|
IsEnabled="{Binding CanAddAlerts}"
|
|
ToolTip="Celebration zone for Super Chat & memberships — one alert layer at a time"/>
|
|
</ContextMenu>
|
|
</Button.ContextMenu>
|
|
<Path Data="M12,4 L12,20 M4,12 L20,12" Stroke="#e94560" StrokeThickness="2"
|
|
Width="14" Height="14" Stretch="Uniform"/>
|
|
</Button>
|
|
</StackPanel>
|
|
|
|
<Helpers:FocusPreservingListBox x:Name="SourceList" Grid.Row="1" Background="Transparent" BorderThickness="0"
|
|
ItemsSource="{Binding StagedScene.Elements}"
|
|
SelectedItem="{Binding SelectedElement, Mode=TwoWay}"
|
|
PreviewMouseLeftButtonDown="List_PreviewMouseLeftButtonDown"
|
|
PreviewMouseMove="List_PreviewMouseMove"
|
|
PreviewMouseLeftButtonUp="List_PreviewMouseLeftButtonUp"
|
|
Foreground="#e0e0e0" FontSize="13">
|
|
<Helpers:FocusPreservingListBox.ItemContainerStyle>
|
|
<Style TargetType="ListBoxItem" BasedOn="{StaticResource YtListBoxItem}">
|
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
|
<Style.Triggers>
|
|
<DataTrigger Binding="{Binding IsVisible}" Value="False">
|
|
<Setter Property="Opacity" Value="0.45"/>
|
|
</DataTrigger>
|
|
</Style.Triggers>
|
|
</Style>
|
|
</Helpers:FocusPreservingListBox.ItemContainerStyle>
|
|
<ListBox.ItemTemplate>
|
|
<DataTemplate>
|
|
<Grid Margin="0,2"
|
|
Tag="{Binding DataContext.CanChangeBackground, RelativeSource={RelativeSource AncestorType=Window}}">
|
|
<Grid.ColumnDefinitions>
|
|
<ColumnDefinition Width="*"/>
|
|
<ColumnDefinition Width="Auto"/>
|
|
<ColumnDefinition Width="Auto"/>
|
|
<ColumnDefinition Width="Auto"/>
|
|
</Grid.ColumnDefinitions>
|
|
|
|
<Grid.ContextMenu>
|
|
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
|
<MenuItem Header="Change Capture…" Click="BackgroundMenu_ChangeCapture">
|
|
<MenuItem.Visibility>
|
|
<MultiBinding Converter="{StaticResource AllTrueToVis}">
|
|
<Binding Path="IsBackground"/>
|
|
<Binding Path="PlacementTarget.Tag" RelativeSource="{RelativeSource AncestorType={x:Type ContextMenu}}"/>
|
|
</MultiBinding>
|
|
</MenuItem.Visibility>
|
|
</MenuItem>
|
|
<MenuItem Header="Refresh Capture" Click="BackgroundMenu_RefreshCapture">
|
|
<MenuItem.Visibility>
|
|
<MultiBinding Converter="{StaticResource AllTrueToVis}">
|
|
<Binding Path="IsBackground"/>
|
|
<Binding Path="PlacementTarget.Tag" RelativeSource="{RelativeSource AncestorType={x:Type ContextMenu}}"/>
|
|
</MultiBinding>
|
|
</MenuItem.Visibility>
|
|
</MenuItem>
|
|
<MenuItem Header="Capture Window…"
|
|
ItemsSource="{Binding DataContext.OpenWindows, RelativeSource={RelativeSource AncestorType=Window}}">
|
|
<MenuItem.Visibility>
|
|
<MultiBinding Converter="{StaticResource AllTrueToVis}">
|
|
<Binding Path="IsBackground"/>
|
|
<Binding Path="PlacementTarget.Tag" RelativeSource="{RelativeSource AncestorType={x:Type ContextMenu}}"/>
|
|
</MultiBinding>
|
|
</MenuItem.Visibility>
|
|
<MenuItem.ItemContainerStyle>
|
|
<Style TargetType="MenuItem">
|
|
<Setter Property="Header" Value="{Binding Label}"/>
|
|
<Setter Property="Command"
|
|
Value="{Binding DataContext.CaptureWindowCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
|
<Setter Property="CommandParameter" Value="{Binding}"/>
|
|
</Style>
|
|
</MenuItem.ItemContainerStyle>
|
|
</MenuItem>
|
|
<MenuItem Header="Refresh Window List" Click="BackgroundMenu_RefreshWindowList">
|
|
<MenuItem.Visibility>
|
|
<MultiBinding Converter="{StaticResource AllTrueToVis}">
|
|
<Binding Path="IsBackground"/>
|
|
<Binding Path="PlacementTarget.Tag" RelativeSource="{RelativeSource AncestorType={x:Type ContextMenu}}"/>
|
|
</MultiBinding>
|
|
</MenuItem.Visibility>
|
|
</MenuItem>
|
|
<Separator>
|
|
<Separator.Visibility>
|
|
<MultiBinding Converter="{StaticResource AllTrueToVis}">
|
|
<Binding Path="IsBackground"/>
|
|
<Binding Path="PlacementTarget.Tag" RelativeSource="{RelativeSource AncestorType={x:Type ContextMenu}}"/>
|
|
</MultiBinding>
|
|
</Separator.Visibility>
|
|
</Separator>
|
|
<MenuItem Header="Remove Layer" Click="SourceMenu_Remove"
|
|
Visibility="{Binding IsBackground, Converter={StaticResource InverseBoolToVis}}"/>
|
|
</ContextMenu>
|
|
</Grid.ContextMenu>
|
|
|
|
<TextBlock Grid.Column="0" Text="{Binding Name}" VerticalAlignment="Center"
|
|
Padding="4,2" TextTrimming="CharacterEllipsis">
|
|
<TextBlock.Style>
|
|
<Style TargetType="TextBlock">
|
|
<Setter Property="Visibility" Value="Visible"/>
|
|
<Style.Triggers>
|
|
<DataTrigger Binding="{Binding IsEditing}" Value="True">
|
|
<Setter Property="Visibility" Value="Collapsed"/>
|
|
</DataTrigger>
|
|
</Style.Triggers>
|
|
</Style>
|
|
</TextBlock.Style>
|
|
</TextBlock>
|
|
|
|
<TextBox Grid.Column="0" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
|
|
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="0"
|
|
Padding="4,2" VerticalContentAlignment="Center"
|
|
IsVisibleChanged="SourceNameBox_IsVisibleChanged"
|
|
KeyDown="SourceNameBox_KeyDown" LostFocus="SourceNameBox_LostFocus">
|
|
<TextBox.Style>
|
|
<Style TargetType="TextBox">
|
|
<Setter Property="Visibility" Value="Collapsed"/>
|
|
<Style.Triggers>
|
|
<DataTrigger Binding="{Binding IsEditing}" Value="True">
|
|
<Setter Property="Visibility" Value="Visible"/>
|
|
</DataTrigger>
|
|
</Style.Triggers>
|
|
</Style>
|
|
</TextBox.Style>
|
|
</TextBox>
|
|
|
|
<Button Grid.Column="1" ToolTip="Edit Layer Name"
|
|
Command="{Binding DataContext.EditElementCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
|
CommandParameter="{Binding}" Style="{StaticResource IconButton}">
|
|
<Path Data="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"
|
|
Fill="#d0d0d0" Width="13" Height="13" Stretch="Uniform"/>
|
|
</Button>
|
|
|
|
<Button Grid.Column="2" Style="{StaticResource EyeButton}"
|
|
Command="{Binding DataContext.ToggleElementVisibilityCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
|
CommandParameter="{Binding}">
|
|
<Path Style="{StaticResource EyeIconStyle}" Fill="#d0d0d0"
|
|
Width="13" Height="13" Stretch="Uniform"/>
|
|
</Button>
|
|
|
|
<Button Grid.Column="3" ToolTip="Remove Layer"
|
|
Command="{Binding DataContext.RemoveSourceCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
|
CommandParameter="{Binding}" Style="{StaticResource IconButton}"
|
|
Visibility="{Binding IsBackground, Converter={StaticResource HiddenBoolToVis}}">
|
|
<Path Data="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"
|
|
Fill="#d0d0d0" Width="13" Height="13" Stretch="Uniform"/>
|
|
</Button>
|
|
</Grid>
|
|
</DataTemplate>
|
|
</ListBox.ItemTemplate>
|
|
</Helpers:FocusPreservingListBox>
|
|
|
|
<TextBlock Grid.Row="2" Text="No layers yet" Foreground="#555" FontSize="12"
|
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
|
Visibility="{Binding ShowSourcesEmptyHint, Converter={StaticResource BoolToVis}}"/>
|
|
|
|
<!-- Resource alert: pinned to the bottom of the Layers panel.
|
|
Red when a required resource (the one-and-only webcam)
|
|
could not be locked; persists and re-polls until resolved. -->
|
|
<Border Grid.Row="3" Margin="0,8,0,0" Padding="8,6" CornerRadius="4"
|
|
Background="#3a1216" BorderBrush="#e94560" BorderThickness="1"
|
|
Visibility="{Binding WebcamLockAlert, Converter={StaticResource NotNullToVis}}">
|
|
<StackPanel>
|
|
<TextBlock Text="{Binding WebcamLockAlert}" Foreground="#ffb4b4" FontSize="11"
|
|
TextWrapping="Wrap"/>
|
|
<Button Content="Retry" Command="{Binding RetryWebcamLockCommand}"
|
|
Style="{StaticResource YtButtonSecondary}" Padding="10,2"
|
|
HorizontalAlignment="Right" Margin="0,6,0,0"/>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!-- ELEMENTS panel: per-element config beneath Sources.
|
|
Collapses entirely when no element is selected. -->
|
|
<Rectangle Grid.Row="4" Height="1" Fill="#2c3a5c" Margin="0,10,0,2"
|
|
Visibility="{Binding SelectedElement, Converter={StaticResource NotNullToVis}}"/>
|
|
<StackPanel Grid.Row="5" Orientation="Horizontal" Margin="0,8,0,0"
|
|
Visibility="{Binding SelectedElement, Converter={StaticResource NotNullToVis}}">
|
|
<TextBlock Text="PROPERTIES" Style="{StaticResource SectionHeader}" VerticalAlignment="Center"/>
|
|
</StackPanel>
|
|
<ScrollViewer x:Name="ElementsScrollViewer" Grid.Row="6" VerticalScrollBarVisibility="Auto" Background="Transparent"
|
|
Visibility="{Binding SelectedElement, Converter={StaticResource NotNullToVis}}">
|
|
<StackPanel x:Name="ElementsPanel" Margin="0,4,0,0">
|
|
<!-- Opacity + mirror/shape (draggable elements: image, webcam) -->
|
|
<StackPanel Visibility="{Binding SelectedElement.IsDraggable, Converter={StaticResource BoolToVis}}" Margin="0,0,0,8">
|
|
<StackPanel Visibility="{Binding SelectedElement.IsChatBox, Converter={StaticResource InverseBoolToVis}}">
|
|
<TextBlock Text="Opacity" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"/>
|
|
<DockPanel Margin="0,0,0,6">
|
|
<TextBlock x:Name="OpacityValueText" Text="100%" Foreground="#a0a0b0" FontSize="11"
|
|
DockPanel.Dock="Right" VerticalAlignment="Center" Margin="8,0,0,0" Width="32"/>
|
|
<Slider x:Name="OpacitySlider" Width="120" Minimum="0" Maximum="1"
|
|
Value="{Binding SelectedElement.Opacity, Mode=TwoWay}"
|
|
ValueChanged="OpacitySlider_ValueChanged" VerticalAlignment="Center"/>
|
|
</DockPanel>
|
|
</StackPanel>
|
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,4"
|
|
Visibility="{Binding IsWebcamSelected, Converter={StaticResource BoolToVis}}">
|
|
<Button Content="{Binding SelectedElement.MirrorButtonText}"
|
|
Style="{StaticResource YtButtonSecondary}" Padding="10,4"
|
|
Click="MirrorButton_Click"/>
|
|
<Button Content="{Binding SelectedElement.ShapeButtonText}"
|
|
Style="{StaticResource YtButtonSecondary}" Padding="10,4"
|
|
Margin="8,0,0,0" Click="ShapeButton_Click"/>
|
|
</StackPanel>
|
|
<TextBlock Text="Drag to move · drag the corner dot to resize"
|
|
Foreground="#a0a0b0" FontSize="10" Margin="0,2,0,0"/>
|
|
</StackPanel>
|
|
|
|
<!-- Webcam border config -->
|
|
<StackPanel Visibility="{Binding SelectedElement.IsWebcam, Converter={StaticResource BoolToVis}}" Margin="0,0,0,8">
|
|
<TextBlock Text="Border Color" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"/>
|
|
<DockPanel Margin="0,0,0,6">
|
|
<Button DockPanel.Dock="Right" Content="..." Width="24" Height="24"
|
|
Click="WebcamBorderColorPicker_Click" Margin="4,0,0,0"
|
|
ToolTip="Pick color"/>
|
|
<TextBox Text="{Binding SelectedElement.BorderColor, UpdateSourceTrigger=PropertyChanged}"
|
|
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
|
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"
|
|
MaxLength="7" ToolTip="#RRGGBB"/>
|
|
</DockPanel>
|
|
<TextBlock Text="Border Thickness" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"/>
|
|
<DockPanel Margin="0,0,0,6">
|
|
<Button DockPanel.Dock="Right" Style="{StaticResource IconButton}"
|
|
Width="22" Height="22" Margin="4,0,0,0"
|
|
Click="WebcamBorderAccept_Click">
|
|
<Path Data="M4,9 L8,14 L16,4" Stroke="#22c55e" StrokeThickness="2"
|
|
Width="14" Height="14" Stretch="Uniform"/>
|
|
</Button>
|
|
<Button DockPanel.Dock="Right" Style="{StaticResource IconButton}"
|
|
Width="22" Height="22" ToolTip="Revert"
|
|
Click="WebcamBorderRevert_Click" Margin="4,0,0,0">
|
|
<Path Data="M5,5 L15,15 M15,5 L5,15" Stroke="#e94560" StrokeThickness="2"
|
|
Width="14" Height="14" Stretch="Uniform"/>
|
|
</Button>
|
|
<Slider DockPanel.Dock="Left" Width="120" Minimum="0" Maximum="10"
|
|
Value="{Binding SelectedElement.BorderWidth, Mode=TwoWay}"
|
|
VerticalAlignment="Center" TickFrequency="1" IsSnapToTickEnabled="True"/>
|
|
<TextBlock Text="{Binding SelectedElement.BorderWidth}" Foreground="#a0a0b0" FontSize="11"
|
|
VerticalAlignment="Center" Margin="8,0,0,0" Width="20"/>
|
|
</DockPanel>
|
|
</StackPanel>
|
|
|
|
<!-- Countdown config -->
|
|
<StackPanel Visibility="{Binding SelectedElement.IsCountdown, Converter={StaticResource BoolToVis}}" Margin="0,0,0,8">
|
|
<TextBlock Text="Timer (minutes)" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"/>
|
|
<DockPanel Margin="0,0,0,6">
|
|
<Button DockPanel.Dock="Right" Style="{StaticResource IconButton}"
|
|
Width="22" Height="22" Margin="4,0,0,0"
|
|
Click="CountdownAccept_Click">
|
|
<Path Data="M4,9 L8,14 L16,4" Stroke="#22c55e" StrokeThickness="2"
|
|
Width="14" Height="14" Stretch="Uniform"/>
|
|
</Button>
|
|
<Button DockPanel.Dock="Right" Style="{StaticResource IconButton}"
|
|
Width="22" Height="22" ToolTip="Revert"
|
|
Click="CountdownRevert_Click" Margin="4,0,0,0">
|
|
<Path Data="M5,5 L15,15 M15,5 L5,15" Stroke="#e94560" StrokeThickness="2"
|
|
Width="14" Height="14" Stretch="Uniform"/>
|
|
</Button>
|
|
<Slider DockPanel.Dock="Left" Width="120" Minimum="1" Maximum="60"
|
|
Value="{Binding SelectedElement.TimerMinutes, Mode=TwoWay}"
|
|
VerticalAlignment="Center" TickFrequency="1" IsSnapToTickEnabled="True"/>
|
|
<TextBlock Text="{Binding SelectedElement.TimerMinutes}" Foreground="#a0a0b0" FontSize="11"
|
|
VerticalAlignment="Center" Margin="8,0,0,0" Width="20"/>
|
|
</DockPanel>
|
|
</StackPanel>
|
|
|
|
<!-- Web source config -->
|
|
<StackPanel Visibility="{Binding SelectedElement.IsWebSource, Converter={StaticResource BoolToVis}}" Margin="0,0,0,8">
|
|
<TextBlock Text="Geometry" Foreground="#a0a0b0" FontSize="11" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="X" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="16"/>
|
|
<TextBox Text="{Binding SelectedElement.X, UpdateSourceTrigger=PropertyChanged, StringFormat=F0}"
|
|
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
|
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"/>
|
|
<TextBlock DockPanel.Dock="Left" Text=" Y" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="20" Margin="8,0,0,0"/>
|
|
<TextBox Text="{Binding SelectedElement.Y, UpdateSourceTrigger=PropertyChanged, StringFormat=F0}"
|
|
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
|
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"/>
|
|
</DockPanel>
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="W" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="16"/>
|
|
<TextBox Text="{Binding SelectedElement.Width, UpdateSourceTrigger=PropertyChanged, StringFormat=F0}"
|
|
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
|
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"/>
|
|
<TextBlock DockPanel.Dock="Left" Text=" H" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="20" Margin="8,0,0,0"/>
|
|
<TextBox Text="{Binding SelectedElement.Height, UpdateSourceTrigger=PropertyChanged, StringFormat=F0}"
|
|
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
|
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"/>
|
|
</DockPanel>
|
|
<TextBlock Text="URI" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"/>
|
|
<DockPanel>
|
|
<Button DockPanel.Dock="Right" Style="{StaticResource IconButton}"
|
|
Width="22" Height="22" Margin="4,0,0,0" IsTabStop="False"
|
|
Click="WebAccept_Click">
|
|
<Path Data="M4,9 L8,14 L16,4" Stroke="#22c55e" StrokeThickness="2"
|
|
Width="14" Height="14" Stretch="Uniform"/>
|
|
</Button>
|
|
<Button DockPanel.Dock="Right" Style="{StaticResource IconButton}"
|
|
Width="22" Height="22" ToolTip="Revert" IsTabStop="False"
|
|
Click="WebRevert_Click" Margin="4,0,0,0">
|
|
<Path Data="M5,5 L15,15 M15,5 L5,15" Stroke="#e94560" StrokeThickness="2"
|
|
Width="14" Height="14" Stretch="Uniform"/>
|
|
</Button>
|
|
<TextBox Text="{Binding SelectedElement.WebUri, UpdateSourceTrigger=PropertyChanged}"
|
|
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
|
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"
|
|
ToolTip="https://example.com"/>
|
|
</DockPanel>
|
|
</StackPanel>
|
|
|
|
<!-- Chat box config -->
|
|
<StackPanel Visibility="{Binding SelectedElement.IsChatBox, Converter={StaticResource BoolToVis}}" Margin="0,0,0,8">
|
|
<TextBlock Text="Chat Overlay" Foreground="#a0a0b0" FontSize="11" FontWeight="SemiBold" Margin="0,0,0,6"/>
|
|
|
|
<!-- Font Size -->
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Font Size" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
|
<Slider x:Name="ChatFontSizeSlider" Width="120" Minimum="8" Maximum="72" TickFrequency="2" IsSnapToTickEnabled="True"
|
|
Value="{Binding SelectedElement.ChatFontSize, Mode=TwoWay}"
|
|
VerticalAlignment="Center"/>
|
|
<TextBlock DockPanel.Dock="Right" Text="{Binding SelectedElement.ChatFontSize, StringFormat={}{0:0}}" Foreground="#e0e0e0" FontSize="11" Margin="6,0,0,0" VerticalAlignment="Center" Width="24"/>
|
|
</DockPanel>
|
|
|
|
<!-- Font Family -->
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Font" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
|
<ComboBox SelectedValue="{Binding SelectedElement.ChatFontFamily, Mode=TwoWay}"
|
|
SelectedValuePath="Content"
|
|
Background="#0f3460" Foreground="#e0e0e0" FontSize="11"
|
|
Width="140" VerticalAlignment="Center">
|
|
<ComboBoxItem Content="Segoe UI"/>
|
|
<ComboBoxItem Content="Arial"/>
|
|
<ComboBoxItem Content="Consolas"/>
|
|
<ComboBoxItem Content="Courier New"/>
|
|
<ComboBoxItem Content="Comic Sans MS"/>
|
|
<ComboBoxItem Content="Mouse Memoirs"/>
|
|
</ComboBox>
|
|
</DockPanel>
|
|
|
|
<!-- Font Color -->
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Font Color" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
|
<TextBox Text="{Binding SelectedElement.ChatFontColor, UpdateSourceTrigger=PropertyChanged}"
|
|
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="1"
|
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="11" Width="80"/>
|
|
</DockPanel>
|
|
|
|
<!-- Member Color -->
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Member" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
|
<TextBox Text="{Binding SelectedElement.ChatMemberColor, UpdateSourceTrigger=PropertyChanged}"
|
|
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="1"
|
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="11" Width="80"/>
|
|
</DockPanel>
|
|
|
|
<!-- Super Chat Color -->
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Super Chat" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
|
<TextBox Text="{Binding SelectedElement.ChatSuperChatColor, UpdateSourceTrigger=PropertyChanged}"
|
|
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="1"
|
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="11" Width="80"/>
|
|
</DockPanel>
|
|
|
|
<!-- Max Messages -->
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Max Msgs" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
|
<Slider x:Name="ChatMaxMsgsSlider" Width="120" Minimum="1" Maximum="20" TickFrequency="1" IsSnapToTickEnabled="True"
|
|
Value="{Binding SelectedElement.ChatMaxMessages, Mode=TwoWay}"
|
|
VerticalAlignment="Center"/>
|
|
<TextBlock DockPanel.Dock="Right" Text="{Binding SelectedElement.ChatMaxMessages}" Foreground="#e0e0e0" FontSize="11" Margin="6,0,0,0" VerticalAlignment="Center" Width="24"/>
|
|
</DockPanel>
|
|
|
|
<!-- Background Color -->
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Background" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
|
<TextBox Text="{Binding SelectedElement.ChatBackgroundColor, UpdateSourceTrigger=PropertyChanged}"
|
|
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="1"
|
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="11" Width="80"/>
|
|
</DockPanel>
|
|
|
|
<!-- Background Opacity -->
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Bg Opacity" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
|
<Slider x:Name="ChatBgOpacitySlider" Width="120" Minimum="0" Maximum="1"
|
|
Value="{Binding SelectedElement.ChatBackgroundOpacity, Mode=TwoWay}"
|
|
VerticalAlignment="Center"/>
|
|
<TextBlock DockPanel.Dock="Right" Text="{Binding SelectedElement.ChatBackgroundOpacity, StringFormat={}{0:0%}}" Foreground="#e0e0e0" FontSize="11" Margin="6,0,0,0" VerticalAlignment="Center" Width="32"/>
|
|
</DockPanel>
|
|
|
|
<!-- Fade After (seconds) -->
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Fade After" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
|
<Slider x:Name="ChatFadeSlider" Width="120" Minimum="0" Maximum="60" TickFrequency="5" IsSnapToTickEnabled="True"
|
|
Value="{Binding SelectedElement.ChatFadeSeconds, Mode=TwoWay}"
|
|
VerticalAlignment="Center"/>
|
|
<TextBlock DockPanel.Dock="Right" Text="{Binding SelectedElement.ChatFadeSeconds, StringFormat='{}{0}s'}" Foreground="#e0e0e0" FontSize="11" Margin="6,0,0,0" VerticalAlignment="Center" Width="28"/>
|
|
</DockPanel>
|
|
|
|
<!-- Toggles -->
|
|
<CheckBox Content="Show timestamps" Foreground="#a0a0b0" FontSize="11" Margin="0,4,0,2"
|
|
IsChecked="{Binding SelectedElement.ChatShowTimestamps, Mode=TwoWay}"/>
|
|
<CheckBox Content="Show badges" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"
|
|
IsChecked="{Binding SelectedElement.ChatShowBadges, Mode=TwoWay}"/>
|
|
</StackPanel>
|
|
|
|
<!-- Stream Alerts config -->
|
|
<StackPanel Visibility="{Binding SelectedElement.IsAlertBox, Converter={StaticResource BoolToVis}}" Margin="0,0,0,8">
|
|
<TextBlock Text="Stream Alerts" Foreground="#a0a0b0" FontSize="11" FontWeight="SemiBold" Margin="0,0,0,6"/>
|
|
<TextBlock Text="The built-in clip plays on every alert (Super Chat, membership…) unless a custom video is picked."
|
|
Foreground="#888" FontSize="10" TextWrapping="Wrap" Margin="0,0,0,6"/>
|
|
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Built-in video" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="84"/>
|
|
<ToggleButton Style="{StaticResource PillToggle}"
|
|
IsChecked="{Binding SelectedElement.AlertUseDefaultVideo, Mode=TwoWay}"
|
|
HorizontalAlignment="Left"/>
|
|
</DockPanel>
|
|
|
|
<Grid Margin="0,0,0,4"
|
|
Visibility="{Binding SelectedElement.AlertUseDefaultVideo, Converter={StaticResource InverseBoolToVis}}">
|
|
<Grid.ColumnDefinitions>
|
|
<ColumnDefinition Width="*"/>
|
|
<ColumnDefinition Width="Auto"/>
|
|
</Grid.ColumnDefinitions>
|
|
<TextBox Grid.Column="0" Text="{Binding SelectedElement.AlertVideoPath, Mode=OneWay}"
|
|
IsReadOnly="True" Background="#0f3460" Foreground="#a0a0b0"
|
|
FontSize="11" VerticalAlignment="Center"/>
|
|
<Button Grid.Column="1" Content="Browse…" Command="{Binding BrowseAlertVideoCommand}"
|
|
CommandParameter="{Binding SelectedElement}" Style="{StaticResource YtButtonSecondary}"
|
|
Margin="4,0,0,0" Padding="8,2"/>
|
|
</Grid>
|
|
|
|
<Button Content="Reset to built-in" Command="{Binding ResetAlertVideoCommand}"
|
|
CommandParameter="{Binding SelectedElement}" Style="{StaticResource YtButtonSecondary}"
|
|
HorizontalAlignment="Left" Padding="8,2" Margin="0,0,4,0"/>
|
|
<TextBlock Text="Custom clips are referenced from disk — never stored in the layout."
|
|
Foreground="#888" FontSize="10" TextWrapping="Wrap" Margin="0,4,0,6"/>
|
|
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Message ticker" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="84"/>
|
|
<ToggleButton Style="{StaticResource PillToggle}"
|
|
IsChecked="{Binding SelectedElement.AlertShowTicker, Mode=TwoWay}"
|
|
HorizontalAlignment="Left"/>
|
|
</DockPanel>
|
|
|
|
<!-- Announcement display: scroll (marquee), flash (pulse) or
|
|
solid (still). Inline items, not a bound ItemsSource —
|
|
same shape as the chat Font selector above. -->
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Display" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="84"/>
|
|
<ComboBox SelectedValue="{Binding SelectedElement.AlertDisplayMethod, Mode=TwoWay}"
|
|
SelectedValuePath="Tag"
|
|
Background="#0f3460" Foreground="#e0e0e0" FontSize="11"
|
|
Width="140" VerticalAlignment="Center">
|
|
<ComboBoxItem Content="Scroll" Tag="TickerScroll"/>
|
|
<ComboBoxItem Content="Flash" Tag="Flash"/>
|
|
<ComboBoxItem Content="Solid" Tag="Solid"/>
|
|
</ComboBox>
|
|
</DockPanel>
|
|
|
|
<DockPanel Margin="0,0,0,4">
|
|
<TextBlock DockPanel.Dock="Left" Text="Volume" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="84"/>
|
|
<Slider Width="120" Minimum="0" Maximum="1"
|
|
Value="{Binding SelectedElement.AlertVideoVolume, Mode=TwoWay}"
|
|
VerticalAlignment="Center"/>
|
|
<TextBlock DockPanel.Dock="Right" Text="{Binding SelectedElement.AlertVideoVolume, StringFormat={}{0:0%}}" Foreground="#e0e0e0" FontSize="11" Margin="6,0,0,0" VerticalAlignment="Center" Width="32"/>
|
|
</DockPanel>
|
|
</StackPanel>
|
|
|
|
<!-- Background properties -->
|
|
<StackPanel Margin="0,8,0,0"
|
|
Visibility="{Binding SelectedElement.IsBackground, Converter={StaticResource BoolToVis}}">
|
|
<TextBlock Text="Background" Style="{StaticResource SubHeader}"/>
|
|
<StackPanel Margin="4,0,0,0">
|
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
|
<TextBlock Text="Use default background:" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center"/>
|
|
<ToggleButton Style="{StaticResource PillToggle}"
|
|
IsChecked="{Binding SelectedElement.UseDefaultBackground, Mode=TwoWay}"
|
|
Margin="8,0,0,0"/>
|
|
</StackPanel>
|
|
<Grid Margin="0,0,0,4"
|
|
Visibility="{Binding SelectedElement.UseDefaultBackground, Converter={StaticResource InverseBoolToVis}}">
|
|
<Grid.ColumnDefinitions>
|
|
<ColumnDefinition Width="*"/>
|
|
<ColumnDefinition Width="Auto"/>
|
|
</Grid.ColumnDefinitions>
|
|
<TextBox Grid.Column="0" Text="{Binding SelectedElement.CustomBackgroundPath, Mode=OneWay}"
|
|
IsReadOnly="True" Background="#0f3460" Foreground="#a0a0b0"
|
|
FontSize="11" VerticalAlignment="Center"/>
|
|
<Button Grid.Column="1" Content="Browse…" Command="{Binding BrowseBackgroundCommand}"
|
|
Style="{StaticResource YtButtonSecondary}" Margin="4,0,0,0" Padding="8,2"/>
|
|
</Grid>
|
|
<TextBlock Text="{Binding BackgroundSizeMismatchText}"
|
|
Foreground="#ef4444" FontSize="11" TextWrapping="Wrap"
|
|
Margin="0,4,0,0"
|
|
Visibility="{Binding BackgroundSizeMismatch, Converter={StaticResource BoolToVis}}"/>
|
|
</StackPanel>
|
|
</StackPanel>
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
</Grid>
|
|
|
|
<!-- Live mode: YouTube Live Chat (entire left panel replaced) -->
|
|
<Grid Grid.Row="0" Visibility="{Binding IsEditMode, Converter={StaticResource InverseBoolToVis}}">
|
|
<Grid.RowDefinitions>
|
|
<RowDefinition Height="Auto"/>
|
|
<RowDefinition Height="*"/>
|
|
</Grid.RowDefinitions>
|
|
|
|
<TextBlock Grid.Row="0" Text="LIVE CHAT" Style="{StaticResource SectionHeader}"/>
|
|
|
|
<!-- Inactive overlay (shown when NOT live) -->
|
|
<StackPanel Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Center"
|
|
Visibility="{Binding ShowChatInactiveMessage, Converter={StaticResource BoolToVis}}">
|
|
<TextBlock Text="Chat is inactive" Foreground="#a0a0b0" FontSize="13"
|
|
HorizontalAlignment="Center" Margin="0,0,0,4"/>
|
|
<TextBlock Text="Chat becomes active once LlamaCasty is connected and streaming on YouTube."
|
|
Foreground="#666" FontSize="11" TextWrapping="Wrap"
|
|
HorizontalAlignment="Center" TextAlignment="Center" MaxWidth="240"/>
|
|
</StackPanel>
|
|
|
|
<!-- Message feed (shown when live) -->
|
|
<ListBox Grid.Row="1" Background="Transparent" BorderThickness="0"
|
|
ItemsSource="{Binding ChatMessages}"
|
|
ItemContainerStyle="{StaticResource YtListBoxItem}"
|
|
Visibility="{Binding ShowChatInactiveMessage, Converter={StaticResource InverseBoolToVis}}"
|
|
x:Name="ChatListBox">
|
|
<ListBox.ItemTemplate><DataTemplate>
|
|
<Border x:Name="MessageBorder" Padding="6,4" CornerRadius="4">
|
|
<Border.Style>
|
|
<Style TargetType="Border">
|
|
<Setter Property="Background" Value="Transparent"/>
|
|
<Style.Triggers>
|
|
<DataTrigger Binding="{Binding IsSuperChat}" Value="True">
|
|
<Setter Property="Background" Value="#2a1a0a"/>
|
|
<Setter Property="BorderBrush" Value="#eab308"/>
|
|
<Setter Property="BorderThickness" Value="1,0,1,0"/>
|
|
</DataTrigger>
|
|
</Style.Triggers>
|
|
</Style>
|
|
</Border.Style>
|
|
<StackPanel>
|
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,2">
|
|
<TextBlock Text="⭐" FontSize="10" Margin="0,0,4,0"
|
|
VerticalAlignment="Center"
|
|
Visibility="{Binding IsMember, Converter={StaticResource BoolToVis}}"/>
|
|
<TextBlock Text="{Binding AuthorName}" Foreground="#e94560"
|
|
FontWeight="Bold" FontSize="12" VerticalAlignment="Center"/>
|
|
<TextBlock Text="{Binding SuperChatAmount, StringFormat=${0:F2}}"
|
|
Foreground="#eab308" FontWeight="Bold" FontSize="12"
|
|
Margin="8,0,0,0" VerticalAlignment="Center"
|
|
Visibility="{Binding IsSuperChat, Converter={StaticResource BoolToVis}}"/>
|
|
<TextBlock Text="{Binding SuperChatCurrency}" Foreground="#eab308"
|
|
FontSize="11" Margin="2,0,0,0" VerticalAlignment="Center"
|
|
Visibility="{Binding IsSuperChat, Converter={StaticResource BoolToVis}}"/>
|
|
</StackPanel>
|
|
<TextBlock Text="{Binding Message}" Foreground="#e0e0e0"
|
|
TextWrapping="Wrap" Margin="0,2,0,0"/>
|
|
</StackPanel>
|
|
</Border>
|
|
</DataTemplate></ListBox.ItemTemplate>
|
|
</ListBox>
|
|
</Grid>
|
|
</Grid>
|
|
</Border>
|
|
</UserControl>
|