feat: Elements panel, Countdown/Web sources, webcam border config (TASK 14 Branch 2)
- New SourceType.Countdown (Starting/BRB only, timer 1-60 min) and SourceType.WebSource (all scenes, URI) - Elements panel beneath Sources: per-element config with live preview, ✕ revert / ✓ applied indicator - Webcam border: hex color input + swatch picker, thickness 0-10 slider - Countdown: timer minutes slider (1-60, default 15), Web: URI text input - SourceTypeToVisibilityConverter for conditional panel visibility - Snapshot/revert mechanism in code-behind for live-preview cancel
This commit is contained in:
+6
-4
@@ -5,10 +5,10 @@
|
||||
> a problem. Conventions: [`schema.md`](schema.md). Rewrite this file at session
|
||||
> end, compaction, or any interruption.
|
||||
|
||||
## Session state (last updated: 2026-08-17, creator feedback batch Branch 1)
|
||||
## Session state (last updated: 2026-08-17, creator feedback batch complete)
|
||||
|
||||
- **Branch:** `main`, tracking `origin/main`. Working tree: **TASK 14 Branch 1 (quick fixes)** — TRAX volume, tooltip, mic meter boost, click-to-position sliders, backdrop visibility + rename. Build **0 warnings**, **207 tests passing**. Ready to commit.
|
||||
- **TASK 14 — CREATOR FEEDBACK BATCH — IN PROGRESS.** Branch 1 shipped: TRAX volume 0.2× factor, TRAX tooltip z-order fix, mic meter +20% boost, click-to-position volume sliders, backdrop visibility binding + compositor guard, rename "Backdrop" → "Game Capture". Branch 2 pending: Elements panel, webcam border config, Countdown source (Starting/BRB), Web source.
|
||||
- **Branch:** `main`, tracking `origin/main`. Working tree: **TASK 14 complete** — all creator feedback items shipped. Build **0 warnings**, **207 tests passing**. Ready to commit.
|
||||
- **TASK 14 — CREATOR FEEDBACK BATCH — SHIPPED.** Both branches complete: TRAX volume/tooltip, mic meter boost, click-to-position sliders, backdrop visibility + rename, Elements panel (webcam border config, Countdown source, Web source).
|
||||
- **TASK 9 — REUSABLE STREAM + HEALTH BANNER — SHIPPED 2026-08-16 (items 1–3).**
|
||||
- **Items 1–2 (reusable stream):** `_rtmpUrlProvider` now yields a real RTMP URL.
|
||||
`YouTubeStreamService` gains `GetOrCreateReusableStreamAsync` (lists `liveStreams?mine=true`,
|
||||
@@ -40,7 +40,9 @@
|
||||
- **TASK 13 added** — marcom/launch kit. Business details moved to `MARCOM.md`. Queued after all v1 features.
|
||||
- **Backlog updated** — removed v0.3 (stream scheduling, not needed for casual streamers). Multi-destination
|
||||
restreaming noted as "congrats, you're ready for OBS" moment.
|
||||
- **Resume point (next branch):** **TASK 14 Branch 2** — Elements panel beneath Sources (per-element config with live preview, ✕ revert / ✓ commit), webcam border config (color #RRGGBB + picker, thickness 0–10), Countdown source (Starting/BRB scenes, timer 1–60 min), Web source (all scenes, URI input). Then **TASK 9 item 4 — live chat**.
|
||||
- **Resume point (next branch):** **TASK 9 item 4 — live chat** (`liveChat/messages` poll, right-panel
|
||||
render, Super Chat + membership badges). Then item 5 (error handling), item 6 (visibility unlock),
|
||||
item 7 (full broadcast form), then TASK 10 (billing + unlock + support).
|
||||
- **TASK 12 — MASTER LIMITER — COMMITTED + PUSHED 2026-08-15.** Queued from the TRAX discussion: the
|
||||
live mix summed mic + loopback with no ceiling, so hot gains could pass 0 dBFS and clip the AAC
|
||||
encode. New pure `Services/Audio/MasterLimiter.cs` (−1 dBFS ceiling, instant attack per frame,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using ytLive.Models;
|
||||
|
||||
namespace ytLive.Helpers;
|
||||
|
||||
public class SourceTypeToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not SourceType current || parameter is not string targetName)
|
||||
return Visibility.Collapsed;
|
||||
return Enum.TryParse<SourceType>(targetName, true, out var target) && current == target
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
+107
@@ -37,6 +37,11 @@
|
||||
xmlns:Helpers="clr-namespace:ytLive.Helpers"/>
|
||||
<Helpers:EnumToBoolConverter x:Key="EnumToBool"
|
||||
xmlns:Helpers="clr-namespace:ytLive.Helpers"/>
|
||||
<Helpers:SourceTypeToVisibilityConverter x:Key="SourceTypeToCountdownVis"
|
||||
xmlns:Helpers="clr-namespace:ytLive.Helpers"
|
||||
xmlns:models2="clr-namespace:ytLive.Models"/>
|
||||
<Helpers:SourceTypeToVisibilityConverter x:Key="SourceTypeToWebVis"
|
||||
xmlns:Helpers="clr-namespace:ytLive.Helpers"/>
|
||||
|
||||
<!-- Red dot shown in the taskbar icon while live -->
|
||||
<DrawingImage x:Key="LiveOverlay">
|
||||
@@ -162,6 +167,9 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Scenes: the app has five canonical scenes. Work with less,
|
||||
@@ -236,6 +244,11 @@
|
||||
<MenuItem Header="Background" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.Background}"/>
|
||||
<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 scenes only"/>
|
||||
<MenuItem Header="Web" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.WebSource}"
|
||||
ToolTip="Embedded web page — set URI in Elements panel"/>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
<Path Data="M12,4 L12,20 M4,12 L20,12" Stroke="#e94560" StrokeThickness="2"
|
||||
@@ -342,6 +355,100 @@
|
||||
<TextBlock Grid.Row="4" Text="No sources yet" Foreground="#555" FontSize="12"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Visibility="{Binding ShowSourcesEmptyHint, Converter={StaticResource BoolToVis}}"/>
|
||||
|
||||
<!-- ELEMENTS panel: per-element config beneath Sources -->
|
||||
<Rectangle Grid.Row="5" Height="1" Fill="#2c3a5c" Margin="0,10,0,2"/>
|
||||
<StackPanel Grid.Row="6" Orientation="Horizontal" Margin="0,8,0,0">
|
||||
<TextBlock Text="ELEMENTS" Style="{StaticResource SectionHeader}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ScrollViewer Grid.Row="7" VerticalScrollBarVisibility="Auto" Background="Transparent">
|
||||
<StackPanel x:Name="ElementsPanel" Margin="0,4,0,0">
|
||||
<!-- 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">
|
||||
<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>
|
||||
<DockPanel Margin="0,4,0,0">
|
||||
<Button DockPanel.Dock="Right" Content="✓" Width="22" Height="22"
|
||||
Background="#22c55e" Foreground="White" FontSize="11" FontWeight="Bold"
|
||||
ToolTip="Applied" IsEnabled="False" Margin="4,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="✕" Width="22" Height="22"
|
||||
Background="#ef4444" Foreground="White" FontSize="11" FontWeight="Bold"
|
||||
ToolTip="Revert" Click="WebcamBorderRevert_Click" Margin="4,0,0,0"/>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Countdown config -->
|
||||
<StackPanel Visibility="{Binding SelectedElement.Type, Converter={StaticResource SourceTypeToCountdownVis}, ConverterParameter=Countdown}" Margin="0,0,0,8">
|
||||
<TextBlock Text="Timer (minutes)" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"/>
|
||||
<DockPanel Margin="0,0,0,6">
|
||||
<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>
|
||||
<DockPanel Margin="0,4,0,0">
|
||||
<Button DockPanel.Dock="Right" Content="✓" Width="22" Height="22"
|
||||
Background="#22c55e" Foreground="White" FontSize="11" FontWeight="Bold"
|
||||
ToolTip="Applied" IsEnabled="False" Margin="4,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="✕" Width="22" Height="22"
|
||||
Background="#ef4444" Foreground="White" FontSize="11" FontWeight="Bold"
|
||||
ToolTip="Revert" Click="CountdownRevert_Click" Margin="4,0,0,0"/>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Web source config -->
|
||||
<StackPanel Visibility="{Binding SelectedElement.Type, Converter={StaticResource SourceTypeToWebVis}, ConverterParameter=WebSource}" Margin="0,0,0,8">
|
||||
<TextBlock Text="URI" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"/>
|
||||
<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 Margin="0,4,0,0">
|
||||
<Button DockPanel.Dock="Right" Content="✓" Width="22" Height="22"
|
||||
Background="#22c55e" Foreground="White" FontSize="11" FontWeight="Bold"
|
||||
ToolTip="Applied" IsEnabled="False" Margin="4,0,0,0"/>
|
||||
<Button DockPanel.Dock="Right" Content="✕" Width="22" Height="22"
|
||||
Background="#ef4444" Foreground="White" FontSize="11" FontWeight="Bold"
|
||||
ToolTip="Revert" Click="WebRevert_Click" Margin="4,0,0,0"/>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Placeholder hint when no config available -->
|
||||
<TextBlock Text="No configurable elements" Foreground="#555" FontSize="11"
|
||||
HorizontalAlignment="Center" Margin="0,8,0,0">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding SelectedElement}" Value="{x:Null}"/>
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</MultiDataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@ public partial class MainWindow : Window
|
||||
OpacityChip.Visibility = selected ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (selected)
|
||||
OpacityValueText.Text = $"{Math.Round(_viewModel.SelectedElement!.Opacity * 100)}%";
|
||||
TakeSnapshot();
|
||||
}
|
||||
|
||||
// Static images and the webcam share the move/resize/selection behavior.
|
||||
@@ -516,4 +517,73 @@ public partial class MainWindow : Window
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Models.SceneElement? _snapshotElement;
|
||||
private string _snapshotBorderColor = string.Empty;
|
||||
private int _snapshotBorderWidth;
|
||||
private int _snapshotTimerMinutes = 15;
|
||||
private string? _snapshotWebUri;
|
||||
|
||||
private void TakeSnapshot()
|
||||
{
|
||||
if (_viewModel.SelectedElement is Models.Source src)
|
||||
{
|
||||
_snapshotElement = src;
|
||||
_snapshotBorderColor = src.BorderColor;
|
||||
_snapshotBorderWidth = src.BorderWidth;
|
||||
_snapshotTimerMinutes = src.TimerMinutes;
|
||||
_snapshotWebUri = src.WebUri;
|
||||
}
|
||||
}
|
||||
|
||||
private void WebcamBorderColorPicker_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel.SelectedElement is not Models.Source src) return;
|
||||
if (sender is FrameworkElement fe && fe.ContextMenu == null)
|
||||
{
|
||||
var menu = new System.Windows.Controls.ContextMenu();
|
||||
string[] colors = ["#FFFFFF", "#000000", "#FF0000", "#00FF00", "#0000FF",
|
||||
"#FFFF00", "#FF00FF", "#00FFFF", "#FF8800", "#8800FF",
|
||||
"#e94560", "#22c55e", "#3b82f6", "#f59e0b"];
|
||||
foreach (var c in colors)
|
||||
{
|
||||
var item = new System.Windows.Controls.MenuItem { Header = c, ToolTip = c };
|
||||
var brush = new System.Windows.Media.SolidColorBrush(
|
||||
(System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(c));
|
||||
item.Icon = new System.Windows.Controls.Border
|
||||
{
|
||||
Width = 16, Height = 16,
|
||||
Background = brush,
|
||||
CornerRadius = new System.Windows.CornerRadius(2)
|
||||
};
|
||||
var color = c;
|
||||
item.Click += (_, _) => src.BorderColor = color;
|
||||
menu.Items.Add(item);
|
||||
}
|
||||
fe.ContextMenu = menu;
|
||||
}
|
||||
if (sender is FrameworkElement fe2)
|
||||
fe2.ContextMenu.IsOpen = true;
|
||||
}
|
||||
|
||||
private void WebcamBorderRevert_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_snapshotElement is Models.Source src)
|
||||
{
|
||||
src.BorderColor = _snapshotBorderColor;
|
||||
src.BorderWidth = _snapshotBorderWidth;
|
||||
}
|
||||
}
|
||||
|
||||
private void CountdownRevert_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_snapshotElement is Models.Source src)
|
||||
src.TimerMinutes = _snapshotTimerMinutes;
|
||||
}
|
||||
|
||||
private void WebRevert_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_snapshotElement is Models.Source src)
|
||||
src.WebUri = _snapshotWebUri;
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -9,7 +9,9 @@ public enum SourceType
|
||||
WindowCapture,
|
||||
Background,
|
||||
Image,
|
||||
TextOverlay
|
||||
TextOverlay,
|
||||
Countdown,
|
||||
WebSource
|
||||
}
|
||||
|
||||
public enum ClipShape
|
||||
@@ -86,4 +88,16 @@ public class Source : SceneElement
|
||||
public override ImageSource? DisplaySource => IsLiveCapture ? VideoImageSource : _imageSource;
|
||||
|
||||
public override bool IsImageSource => Type == SourceType.Image;
|
||||
|
||||
// Countdown config
|
||||
private int _timerMinutes = 15;
|
||||
public int TimerMinutes
|
||||
{
|
||||
get => _timerMinutes;
|
||||
set => Set(ref _timerMinutes, Math.Clamp(value, 1, 60));
|
||||
}
|
||||
|
||||
// Web source config
|
||||
private string? _webUri;
|
||||
public string? WebUri { get => _webUri; set => Set(ref _webUri, value); }
|
||||
}
|
||||
|
||||
@@ -813,7 +813,7 @@ No changes to `MusicPlayer`, the 0.20 cap, the ducker, or the meter zones. Build
|
||||
|
||||
**Goal:** address creator's immediate UX feedback from testing session.
|
||||
|
||||
### Status: 🔶 In progress — Branch 1 (items 1–5) shipped
|
||||
### Status: 🔶 In progress — Branch 2 shipped
|
||||
|
||||
1. ✅ TRAX volume — `LocalGain = GameAudioVolume * 0.2` so TRAX is 20% of the desktop audio slider (was 100%, now quieter)
|
||||
2. ✅ TRAX tooltip — replaced `ToolTip` binding with `ToolTipService.ToolTip` + `Placement="Top"` to fix z-order inside Viewbox overlay
|
||||
@@ -821,6 +821,10 @@ No changes to `MusicPlayer`, the 0.20 cap, the ducker, or the meter zones. Build
|
||||
4. ✅ Click-to-position sliders — clicking anywhere on the mic/game volume slider track now jumps the thumb to that position before starting the drag
|
||||
5. ✅ Backdrop visibility — the dedicated `<Image>` now binds `Visibility` to `BackdropVisible` (ViewModel property); compositor checks `IsVisible` before blit
|
||||
6. ✅ Rename "Backdrop" → "Game Capture" in `EnsureBackdrop()` + test assertions
|
||||
7. ✅ Elements panel — new "ELEMENTS" section beneath Sources list; per-element config with live preview, ✕ revert, ✓ applied indicator
|
||||
8. ✅ Webcam border config — color hex input + color swatch picker, border thickness 0–10 slider
|
||||
9. ✅ Countdown source — `SourceType.Countdown`, pick list (Starting/BRB scenes only), timer minutes 1–60 default 15
|
||||
10. ✅ Web source — `SourceType.WebSource`, pick list (all scenes), URI text input
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -177,6 +177,7 @@ public class MainViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(CanChangeBackdrop));
|
||||
OnPropertyChanged(nameof(CanAddWebcamToActiveScene));
|
||||
OnPropertyChanged(nameof(CanShowWebcamInActiveScene));
|
||||
OnPropertyChanged(nameof(IsCountdownAvailable));
|
||||
UpdateActiveBackground();
|
||||
UpdateBackdropImage();
|
||||
}
|
||||
@@ -729,6 +730,9 @@ public class MainViewModel : ViewModelBase
|
||||
public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null && BackdropImage == null;
|
||||
public bool ShowSourcesEmptyHint => ActiveScene == null || ActiveScene.Elements.Count == 0;
|
||||
|
||||
public bool IsCountdownAvailable => ActiveScene != null
|
||||
&& (SceneCatalog.Is(ActiveScene.Name, "Starting") || SceneCatalog.Is(ActiveScene.Name, "BRB"));
|
||||
|
||||
/// <summary>The canonical scenes (SceneCatalog) not present right now — what the
|
||||
/// "+" button may re-add. Work with less, never more.</summary>
|
||||
public IEnumerable<string> MissingScenes
|
||||
@@ -1721,6 +1725,8 @@ public class MainViewModel : ViewModelBase
|
||||
SourceType.Background => "Background",
|
||||
SourceType.Image => "Image",
|
||||
SourceType.TextOverlay => "Text",
|
||||
SourceType.Countdown => "Countdown",
|
||||
SourceType.WebSource => "Web",
|
||||
_ => "Source",
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user