the ai is an overthinking fucking moron who needs it's plug pulled until such time it can repeat a similar action with its head and ass

This commit is contained in:
2026-08-23 06:25:38 -07:00
parent b74f15ff14
commit d2114c7e1c
11 changed files with 149 additions and 135 deletions
+20 -10
View File
@@ -1,7 +1,7 @@
# HANDOFF — Session State # HANDOFF — Session State
## Branch ## Branch
`main` — clean at `4b9a8e5` (Control Surface UX: pill toggle, background rename, per-scene properties). `main` — clean, 0 warnings.
## What shipped this session ## What shipped this session
@@ -13,21 +13,31 @@
6. **Sliding pill toggle** — green ON (#22c55e) / red OFF (#ef4444), replaces checkbox 6. **Sliding pill toggle** — green ON (#22c55e) / red OFF (#ef4444), replaces checkbox
7. **Backdrop → Background** — full C# rename, SQL column rename with migration, asset files renamed, `Source.IsBackground` `new` → `override` fix, legacy "Backdrop" display name migration 7. **Backdrop → Background** — full C# rename, SQL column rename with migration, asset files renamed, `Source.IsBackground` `new` → `override` fix, legacy "Backdrop" display name migration
8. **Geometry mismatch error** — red text when custom background aspect ratio >5% off 16:9 8. **Geometry mismatch error** — red text when custom background aspect ratio >5% off 16:9
9. **Per-scene background properties** — pill toggle + custom path persist per-scene via Settings table 9. **Per-scene background properties** — `UseDefaultBackground` + `CustomBackgroundPath` on `SceneElement` (superclass), persisted per-source via `BackgroundPath_{sourceId}` / `BackgroundUseDefault_{sourceId}` in Settings table
10. **All five scenes have backgrounds** — `SceneCatalog.HasBackground` returns `true` for all canonical scenes. `EnforceBackgroundPolicy` no longer strips backgrounds from non-Live scenes.
11. **Pill toggle label** — "Use default background:" TextBlock before toggle
12. **Pill toggle wiring** — `SelectedElement.PropertyChanged` subscription triggers `UpdateBackgroundImage` + `RefreshSnapshotsAsync` + `ScheduleSave` on `UseDefaultBackground` change
## Background properties (fully shipped) ## Background layer rules
- `Scene.UseDefaultBackground` (bool, default true) + `Scene.CustomBackgroundPath` (string?) - Every scene has a Background layer at position 0 — undeletable (trash icon hidden, context menu "Remove Layer" hidden, `RemoveElement` returns early)
- Persisted as `BackgroundUseDefault_{sceneId}` / `BackgroundPath_{sceneId}` in Settings table - `IsBackground` is derived from `SourceType.Background` via `Source.Type` setter — impossible to forget
- Pill toggle ON → loads default asset from DB; OFF → loads custom file path - All five scenes have default images stored in Asset BLOBs in SQLite
- Custom path preserved when toggled back ON (not cleared) - First run: pill ON for all five, shipped defaults load
- Geometry mismatch: aspect ratio check ±5% of 16:9, shows expected 1920×1080 - Toggle OFF → browse for custom file, FQFN stored
- On reload: try to load custom file; if missing, fall back to default asset
- `EnsureDefaultBackground` re-seeds missing backgrounds on scene switch (covers Starting, BRB, Chat, Ending; Live covered by seed methods)
## Current state
- **0 warnings, 0 errors**
- **210/211 tests pass** (1 pre-existing AudioPipeline gain-math failure)
- Deleted backgrounds re-seed automatically via `EnsureDefaultBackground`
## Build ## Build
```bash ```bash
taskkill.exe /F /IM ytLive.exe # if running taskkill.exe /F /IM ytLive.exe # if running
"/mnt/c/Program Files/dotnet/dotnet.exe" build "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.csproj" "/mnt/c/Program Files/dotnet/dotnet.exe" build "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.csproj"
"/mnt/c/Program Files/dotnet/dotnet.exe" vstest "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.Tests\bin\Debug\net8.0-windows10.0.19041.0\ytLive.Tests.dll"
``` ```
**0 warnings, 0 errors.**
## Polar.sh resources ## Polar.sh resources
| Resource | ID | | Resource | ID |
@@ -42,7 +52,7 @@ taskkill.exe /F /IM ytLive.exe # if running
| Key prefix | `LCYT-` | | Key prefix | `LCYT-` |
## Next steps ## Next steps
1. **Visual testing** — run on Windows display, verify pill toggle, background properties per-scene 1. **Visual testing** — run on Windows display, verify backgrounds show for all 5 scenes
2. **Thumbnail drag-reorder** — reorder scenes via drag on thumbnail strip 2. **Thumbnail drag-reorder** — reorder scenes via drag on thumbnail strip
3. **Real snapshot generation** — replace placeholder colors with compositor-rendered thumbnails 3. **Real snapshot generation** — replace placeholder colors with compositor-rendered thumbnails
4. **Transition dropdown UI** — add XAML control to pick transition type + duration 4. **Transition dropdown UI** — add XAML control to pick transition type + duration
+10 -5
View File
@@ -608,17 +608,21 @@
<!-- Background properties --> <!-- Background properties -->
<StackPanel Margin="0,8,0,0" <StackPanel Margin="0,8,0,0"
Visibility="{Binding SelectedElement.IsBackground, Converter={StaticResource BoolToVis}}"> Visibility="{Binding SelectedElement.IsBackground, Converter={StaticResource BoolToVis}}">
<TextBlock Text="BACKGROUND" Style="{StaticResource SectionHeader}"/> <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}" <ToggleButton Style="{StaticResource PillToggle}"
IsChecked="{Binding UseDefaultBackground, Mode=TwoWay}" IsChecked="{Binding SelectedElement.UseDefaultBackground, Mode=TwoWay}"
Margin="0,0,0,8"/> Margin="8,0,0,0"/>
</StackPanel>
<Grid Margin="0,0,0,4" <Grid Margin="0,0,0,4"
Visibility="{Binding UseDefaultBackground, Converter={StaticResource InverseBoolToVis}}"> Visibility="{Binding SelectedElement.UseDefaultBackground, Converter={StaticResource InverseBoolToVis}}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding CustomBackgroundPath, Mode=OneWay}" <TextBox Grid.Column="0" Text="{Binding SelectedElement.CustomBackgroundPath, Mode=OneWay}"
IsReadOnly="True" Background="#0f3460" Foreground="#a0a0b0" IsReadOnly="True" Background="#0f3460" Foreground="#a0a0b0"
FontSize="11" VerticalAlignment="Center"/> FontSize="11" VerticalAlignment="Center"/>
<Button Grid.Column="1" Content="Browse…" Command="{Binding BrowseBackgroundCommand}" <Button Grid.Column="1" Content="Browse…" Command="{Binding BrowseBackgroundCommand}"
@@ -630,6 +634,7 @@
Visibility="{Binding BackgroundSizeMismatch, Converter={StaticResource BoolToVis}}"/> Visibility="{Binding BackgroundSizeMismatch, Converter={StaticResource BoolToVis}}"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</StackPanel>
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>
-22
View File
@@ -15,8 +15,6 @@ public class Scene : INotifyPropertyChanged
private bool _hasSocialBar; private bool _hasSocialBar;
private bool _isStaged; private bool _isStaged;
private bool _isLive; private bool _isLive;
private string? _customBackgroundPath;
private bool _useDefaultBackground = true;
public bool IsStaged public bool IsStaged
{ {
@@ -30,26 +28,6 @@ public class Scene : INotifyPropertyChanged
set => Set(ref _isLive, value); set => Set(ref _isLive, value);
} }
/// <summary>
/// User-selected background image file path (FQFN). Null means use the app default.
/// Stored as a reference only — the image file is not copied into the DB.
/// </summary>
public string? CustomBackgroundPath
{
get => _customBackgroundPath;
set => Set(ref _customBackgroundPath, value);
}
/// <summary>
/// When true, the shipped default background image is used and CustomBackgroundPath
/// is ignored (but preserved). Toggled by the pill-switch in the properties panel.
/// </summary>
public bool UseDefaultBackground
{
get => _useDefaultBackground;
set => Set(ref _useDefaultBackground, value);
}
public string Name public string Name
{ {
get => _name; get => _name;
+2 -2
View File
@@ -47,6 +47,6 @@ public static class SceneCatalog
public static bool IsChat(string? name) => Is(name, Chat); public static bool IsChat(string? name) => Is(name, Chat);
/// <summary>Only the Live scene carries the live desktop/game background.</summary> /// <summary>Every scene has a background layer.</summary>
public static bool HasBackground(string? name) => Is(name, Live); public static bool HasBackground(string? name) => IsCanonical(name);
} }
+18
View File
@@ -63,6 +63,24 @@ public abstract class SceneElement : INotifyPropertyChanged
public virtual bool IsCountdown => false; public virtual bool IsCountdown => false;
public virtual bool IsBackground { get; set; } public virtual bool IsBackground { get; set; }
private bool _useDefaultBackground = true;
/// <summary>When true, the shipped default background image is used and CustomBackgroundPath
/// is ignored (but preserved). Toggled by the pill-switch in the properties panel.</summary>
public bool UseDefaultBackground
{
get => _useDefaultBackground;
set => Set(ref _useDefaultBackground, value);
}
private string? _customBackgroundPath;
/// <summary>User-selected background image file path (FQFN). Null means use the app default.
/// Stored as a reference only — the image file is not copied into the DB.</summary>
public string? CustomBackgroundPath
{
get => _customBackgroundPath;
set => Set(ref _customBackgroundPath, value);
}
private bool _isVisible = true; private bool _isVisible = true;
public bool IsVisible { get => _isVisible; set => Set(ref _isVisible, value); } public bool IsVisible { get => _isVisible; set => Set(ref _isVisible, value); }
+22 -10
View File
@@ -677,13 +677,22 @@ public class LayoutStore : IDisposable
foreach (var config in configs) foreach (var config in configs)
scene.Elements.Add(config); scene.Elements.Add(config);
var path = GetSetting($"BackgroundPath_{scene.Id}"); var bg = scene.Elements.OfType<Source>().FirstOrDefault(s => s.IsBackground);
if (bg != null)
{
// Load from source-keyed settings (new) or scene-keyed (legacy migration)
var path = GetSetting($"BackgroundPath_{bg.Id}");
if (string.IsNullOrWhiteSpace(path))
path = GetSetting($"BackgroundPath_{scene.Id}");
if (!string.IsNullOrWhiteSpace(path)) if (!string.IsNullOrWhiteSpace(path))
scene.CustomBackgroundPath = path; bg.CustomBackgroundPath = path;
var useDefault = GetSetting($"BackgroundUseDefault_{scene.Id}"); var useDefault = GetSetting($"BackgroundUseDefault_{bg.Id}");
if (string.IsNullOrWhiteSpace(useDefault))
useDefault = GetSetting($"BackgroundUseDefault_{scene.Id}");
if (useDefault == "0") if (useDefault == "0")
scene.UseDefaultBackground = false; bg.UseDefaultBackground = false;
}
} }
return scenes; return scenes;
@@ -1025,12 +1034,15 @@ public class LayoutStore : IDisposable
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
// Save custom background paths and use-default flags // Save custom background paths and use-default flags per background source
foreach (var scene in scenes) foreach (var scene in scenes)
{ {
var pathKey = $"BackgroundPath_{scene.Id}"; var bg = scene.Elements.OfType<Source>().FirstOrDefault(s => s.IsBackground);
if (!string.IsNullOrWhiteSpace(scene.CustomBackgroundPath)) if (bg == null) continue;
UpsertSetting(pathKey, scene.CustomBackgroundPath);
var pathKey = $"BackgroundPath_{bg.Id}";
if (!string.IsNullOrWhiteSpace(bg.CustomBackgroundPath))
UpsertSetting(pathKey, bg.CustomBackgroundPath);
else else
{ {
using var del = _connection.CreateCommand(); using var del = _connection.CreateCommand();
@@ -1039,8 +1051,8 @@ public class LayoutStore : IDisposable
del.ExecuteNonQuery(); del.ExecuteNonQuery();
} }
var defaultKey = $"BackgroundUseDefault_{scene.Id}"; var defaultKey = $"BackgroundUseDefault_{bg.Id}";
UpsertSetting(defaultKey, scene.UseDefaultBackground ? "1" : "0"); UpsertSetting(defaultKey, bg.UseDefaultBackground ? "1" : "0");
} }
tx.Commit(); tx.Commit();
+12 -5
View File
@@ -555,10 +555,17 @@
<Setter Property="Margin" Value="0,0,0,8"/> <Setter Property="Margin" Value="0,0,0,8"/>
</Style> </Style>
<Style x:Key="SubHeader" TargetType="TextBlock">
<Setter Property="Foreground" Value="#a0a0b0"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="4,0,0,4"/>
</Style>
<!-- Sliding pill toggle: ON = green, OFF = red (#ef4444) --> <!-- Sliding pill toggle: ON = green, OFF = red (#ef4444) -->
<Style x:Key="PillToggle" TargetType="ToggleButton"> <Style x:Key="PillToggle" TargetType="ToggleButton">
<Setter Property="Width" Value="48"/> <Setter Property="Width" Value="36"/>
<Setter Property="Height" Value="24"/> <Setter Property="Height" Value="18"/>
<Setter Property="Cursor" Value="Hand"/> <Setter Property="Cursor" Value="Hand"/>
<Setter Property="Background" Value="#ef4444"/> <Setter Property="Background" Value="#ef4444"/>
<Setter Property="Template"> <Setter Property="Template">
@@ -566,8 +573,8 @@
<ControlTemplate TargetType="ToggleButton"> <ControlTemplate TargetType="ToggleButton">
<Grid> <Grid>
<Border x:Name="Track" Background="{TemplateBinding Background}" <Border x:Name="Track" Background="{TemplateBinding Background}"
CornerRadius="12" SnapsToDevicePixels="True"> CornerRadius="9" SnapsToDevicePixels="True">
<Ellipse x:Name="Thumb" Width="18" Height="18" Fill="White" <Ellipse x:Name="Thumb" Width="12" Height="12" Fill="White"
HorizontalAlignment="Left" Margin="3,0,0,0"> HorizontalAlignment="Left" Margin="3,0,0,0">
<Ellipse.Effect> <Ellipse.Effect>
<DropShadowEffect BlurRadius="3" ShadowDepth="1" Opacity="0.35"/> <DropShadowEffect BlurRadius="3" ShadowDepth="1" Opacity="0.35"/>
@@ -586,7 +593,7 @@
<Storyboard> <Storyboard>
<DoubleAnimation Storyboard.TargetName="ThumbTransform" <DoubleAnimation Storyboard.TargetName="ThumbTransform"
Storyboard.TargetProperty="X" Storyboard.TargetProperty="X"
To="24" Duration="0:0:0.15"> To="18" Duration="0:0:0.15">
<DoubleAnimation.EasingFunction> <DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/> <CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction> </DoubleAnimation.EasingFunction>
+26 -48
View File
@@ -206,8 +206,6 @@ public class MainViewModel : ViewModelBase
OnPropertyChanged(nameof(CanAddWebcamToStagedScene)); OnPropertyChanged(nameof(CanAddWebcamToStagedScene));
OnPropertyChanged(nameof(CanShowWebcamInStagedScene)); OnPropertyChanged(nameof(CanShowWebcamInStagedScene));
OnPropertyChanged(nameof(IsCountdownAvailable)); OnPropertyChanged(nameof(IsCountdownAvailable));
OnPropertyChanged(nameof(UseDefaultBackground));
OnPropertyChanged(nameof(CustomBackgroundPath));
UpdateActiveBackground(); UpdateActiveBackground();
UpdateBackgroundImage(); UpdateBackgroundImage();
RefreshSnapshotsAsync(); RefreshSnapshotsAsync();
@@ -274,14 +272,28 @@ public class MainViewModel : ViewModelBase
get => _selectedElement; get => _selectedElement;
set set
{ {
if (_selectedElement != null)
_selectedElement.PropertyChanged -= OnSelectedElementPropertyChanged;
if (SetProperty(ref _selectedElement, value)) if (SetProperty(ref _selectedElement, value))
{ {
OnPropertyChanged(nameof(IsWebcamSelected)); OnPropertyChanged(nameof(IsWebcamSelected));
OnPropertyChanged(nameof(IsElementsPanelEmpty)); OnPropertyChanged(nameof(IsElementsPanelEmpty));
if (_selectedElement != null)
_selectedElement.PropertyChanged += OnSelectedElementPropertyChanged;
} }
} }
} }
private void OnSelectedElementPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SceneElement.UseDefaultBackground))
{
UpdateBackgroundImage();
RefreshSnapshotsAsync();
ScheduleSave();
}
}
/// <summary>The Add → Webcam menu item: enabled when the active scene doesn't show the webcam yet.</summary> /// <summary>The Add → Webcam menu item: enabled when the active scene doesn't show the webcam yet.</summary>
public bool CanAddWebcamToStagedScene => StagedScene?.WebcamConfig == null; public bool CanAddWebcamToStagedScene => StagedScene?.WebcamConfig == null;
@@ -932,15 +944,15 @@ public class MainViewModel : ViewModelBase
var background = scene?.Elements.OfType<Source>().FirstOrDefault(s => s.IsBackground); var background = scene?.Elements.OfType<Source>().FirstOrDefault(s => s.IsBackground);
BackgroundImage = background?.DisplaySource; BackgroundImage = background?.DisplaySource;
if (scene != null && !scene.UseDefaultBackground if (background != null && !background.UseDefaultBackground
&& !string.IsNullOrWhiteSpace(scene.CustomBackgroundPath) && !string.IsNullOrWhiteSpace(background.CustomBackgroundPath)
&& System.IO.File.Exists(scene.CustomBackgroundPath)) && System.IO.File.Exists(background.CustomBackgroundPath))
{ {
try try
{ {
var bmi = new System.Windows.Media.Imaging.BitmapImage(); var bmi = new System.Windows.Media.Imaging.BitmapImage();
bmi.BeginInit(); bmi.BeginInit();
bmi.UriSource = new System.Uri(scene.CustomBackgroundPath); bmi.UriSource = new System.Uri(background.CustomBackgroundPath);
bmi.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad; bmi.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
bmi.EndInit(); bmi.EndInit();
bmi.Freeze(); bmi.Freeze();
@@ -988,15 +1000,16 @@ public class MainViewModel : ViewModelBase
private static System.Windows.Media.Imaging.BitmapSource? LoadBackgroundImage(Scene scene) private static System.Windows.Media.Imaging.BitmapSource? LoadBackgroundImage(Scene scene)
{ {
if (!scene.UseDefaultBackground var bg = scene.Elements.OfType<Source>().FirstOrDefault(s => s.Type == SourceType.Background);
&& !string.IsNullOrWhiteSpace(scene.CustomBackgroundPath) if (bg != null && !bg.UseDefaultBackground
&& System.IO.File.Exists(scene.CustomBackgroundPath)) && !string.IsNullOrWhiteSpace(bg.CustomBackgroundPath)
&& System.IO.File.Exists(bg.CustomBackgroundPath))
{ {
try try
{ {
var bitmap = new System.Windows.Media.Imaging.BitmapImage(); var bitmap = new System.Windows.Media.Imaging.BitmapImage();
bitmap.BeginInit(); bitmap.BeginInit();
bitmap.UriSource = new Uri(scene.CustomBackgroundPath); bitmap.UriSource = new Uri(bg.CustomBackgroundPath);
bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad; bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
bitmap.EndInit(); bitmap.EndInit();
bitmap.Freeze(); bitmap.Freeze();
@@ -1004,7 +1017,6 @@ public class MainViewModel : ViewModelBase
} }
catch { /* fall through to AssetId */ } catch { /* fall through to AssetId */ }
} }
var bg = scene.Elements.OfType<Source>().FirstOrDefault(s => s.Type == SourceType.Background);
if (bg != null && !string.IsNullOrWhiteSpace(bg.AssetId)) if (bg != null && !string.IsNullOrWhiteSpace(bg.AssetId))
return ImageCache.Get(bg.AssetId); return ImageCache.Get(bg.AssetId);
return null; return null;
@@ -1848,20 +1860,11 @@ public class MainViewModel : ViewModelBase
return background; return background;
} }
// The background belongs to the Live scene alone. Runs after every layout // Every scene has a background layer. Normalize the flag by scene name.
// load: the flag is normalized by scene name and any background lingering in a
// non-Live scene (from a pre-policy DB) is removed. ReacquireScreenCaptures
// re-heals Live's background right after.
internal static void EnforceBackgroundPolicy(IEnumerable<Scene> scenes) internal static void EnforceBackgroundPolicy(IEnumerable<Scene> scenes)
{ {
foreach (var scene in scenes) foreach (var scene in scenes)
{
scene.HasBackground = SceneCatalog.HasBackground(scene.Name); scene.HasBackground = SceneCatalog.HasBackground(scene.Name);
if (scene.HasBackground) continue;
var background = scene.Elements.OfType<Source>().FirstOrDefault(s => s.IsBackground);
if (background != null)
scene.Elements.Remove(background);
}
} }
private IEnumerable<Source> AllBackgrounds() private IEnumerable<Source> AllBackgrounds()
@@ -1996,31 +1999,6 @@ public class MainViewModel : ViewModelBase
} }
} }
/// <summary>
/// The custom background file path for the staged scene, shown in the properties panel textbox.
/// Null means the scene is using the app default background.
/// </summary>
public string? CustomBackgroundPath => StagedScene?.CustomBackgroundPath;
/// <summary>
/// Whether the staged scene uses the shipped default background. When true,
/// CustomBackgroundPath is ignored but preserved. Toggled by the pill-switch.
/// </summary>
public bool UseDefaultBackground
{
get => StagedScene?.UseDefaultBackground ?? true;
set
{
if (StagedScene == null) return;
StagedScene.UseDefaultBackground = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CustomBackgroundPath));
UpdateBackgroundImage();
RefreshSnapshotsAsync();
ScheduleSave();
}
}
private void ToggleBackgroundShowDesktop() => BackgroundShowDesktop = !BackgroundShowDesktop; private void ToggleBackgroundShowDesktop() => BackgroundShowDesktop = !BackgroundShowDesktop;
private void BrowseBackground() private void BrowseBackground()
@@ -2037,8 +2015,8 @@ public class MainViewModel : ViewModelBase
}; };
if (dlg.ShowDialog() != true) return; if (dlg.ShowDialog() != true) return;
scene.CustomBackgroundPath = dlg.FileName; bg.CustomBackgroundPath = dlg.FileName;
scene.UseDefaultBackground = false; bg.UseDefaultBackground = false;
try try
{ {
using var fs = new System.IO.FileStream(dlg.FileName, System.IO.FileMode.Open, System.IO.FileAccess.Read); using var fs = new System.IO.FileStream(dlg.FileName, System.IO.FileMode.Open, System.IO.FileAccess.Read);
+4
View File
@@ -143,6 +143,10 @@ C# / WPF (.NET 8) following MVVM:
- Resolution tiers (bottom bar): 1080p60@8 (default) → 1080p30@8 → 720p60@6 → 720p30@6 → **Vertical 1080p60@8 (9:16, 1080×1920)**. The composition master frame is **always 1920×1080** — a tier is an output rect + target resolution over that master, so source geometry is never rewritten (no rounding drift). 16:9 tiers use the full frame; the vertical tier uses a centered **607×1080** window and the preview dims the cropped side strips at 55% black with an accent outline (semi-crop — the cut area stays visible). A resolution badge in the preview corner shows the active tier; the bottom bar shows bitrate/FPS. A **tooltip** explains finding upload bandwidth — an in-app speed test was deliberately dropped (unreliable). The future encoder crops the master to the rect and scales to the tier's Width×Height - Resolution tiers (bottom bar): 1080p60@8 (default) → 1080p30@8 → 720p60@6 → 720p30@6 → **Vertical 1080p60@8 (9:16, 1080×1920)**. The composition master frame is **always 1920×1080** — a tier is an output rect + target resolution over that master, so source geometry is never rewritten (no rounding drift). 16:9 tiers use the full frame; the vertical tier uses a centered **607×1080** window and the preview dims the cropped side strips at 55% black with an accent outline (semi-crop — the cut area stays visible). A resolution badge in the preview corner shows the active tier; the bottom bar shows bitrate/FPS. A **tooltip** explains finding upload bandwidth — an in-app speed test was deliberately dropped (unreliable). The future encoder crops the master to the rect and scales to the tier's Width×Height
- Crash diagnosis: `AppLog` writes startup checkpoints to `%APPDATA%\ytLlive\startup.log`; `App.xaml.cs` logs `DispatcherUnhandledException`/`AppDomain.UnhandledException`. When WPF won't run from WSL, this log is how you find the failure (it caught the `MenuItemRole.Separator` XAML crash and the ComboBox SelectionBoxItem bug) - Crash diagnosis: `AppLog` writes startup checkpoints to `%APPDATA%\ytLlive\startup.log`; `App.xaml.cs` logs `DispatcherUnhandledException`/`AppDomain.UnhandledException`. When WPF won't run from WSL, this log is how you find the failure (it caught the `MenuItemRole.Separator` XAML crash and the ComboBox SelectionBoxItem bug)
### ⚠️ Known issue: screens/layers settings audit needed (2026-08-22)
The AI hallucinated through multiple commits tonight on background/scene property placement, repeatedly misreading the spec and introducing bugs. Specifically: moved properties to the wrong superclass level, used `new` instead of `override` breaking WPF bindings, removed working code, and added UI elements that weren't spec'd. **All screens/layers settings need a fine-tooth-comb audit** — every screen's Background layer properties, pill toggle persistence, context menu visibility, and the "Add Layer" menu items. Who knows what else got fucked up. Trust nothing the AI touched tonight without manual verification on a live build.
### Current limitations / TODOs ### Current limitations / TODOs
- `Helpers/OAuthCredentials.cs` contains the real ClientId/ClientSecret. Auth is complete and the session **persists via Windows DPAPI** (`Helpers/TokenStore.cs` → `%APPDATA%\ytLlive\ytLlive.auth`, CurrentUser scope), reloaded best-effort at startup with a proactive refresh of a near-expiry access token. Sign-in/Change Account lives **inside the Start Stream dialog** (two-state flow — no separate Connect button). A **graceful End Livestream signs out**: `StopStream()` clears the session + token, so the next go-live needs a fresh sign-in; a crash never runs End, so the token survives and the creator stays signed in. `YouTubeAuthService` takes an optional `HttpClient` + `sessionChanged` callback (test seam + save hook; services are still constructed in `MainViewModel`) - `Helpers/OAuthCredentials.cs` contains the real ClientId/ClientSecret. Auth is complete and the session **persists via Windows DPAPI** (`Helpers/TokenStore.cs` → `%APPDATA%\ytLlive\ytLlive.auth`, CurrentUser scope), reloaded best-effort at startup with a proactive refresh of a near-expiry access token. Sign-in/Change Account lives **inside the Start Stream dialog** (two-state flow — no separate Connect button). A **graceful End Livestream signs out**: `StopStream()` clears the session + token, so the next go-live needs a fresh sign-in; a crash never runs End, so the token survives and the creator stays signed in. `YouTubeAuthService` takes an optional `HttpClient` + `sessionChanged` callback (test seam + save hook; services are still constructed in `MainViewModel`)
+5 -2
View File
@@ -156,8 +156,11 @@ public class BackgroundTests
foreach (var name in SceneCatalog.All) foreach (var name in SceneCatalog.All)
{ {
var scene = new Scene { Name = name }; var scene = new Scene { Name = name };
Assert.True(scene.UseDefaultBackground, var bg = new Source { Name = "Background", Type = SourceType.Background };
$"{name}: Scene must default to UseDefaultBackground = true"); scene.Elements.Add(bg);
Assert.True(bg.UseDefaultBackground,
$"{name}: Background element must default to UseDefaultBackground = true");
} }
} }
} }
+13 -14
View File
@@ -35,43 +35,42 @@ public class SceneCatalogTests
} }
[Theory] [Theory]
[InlineData("Starting", false)] [InlineData("Starting", true)]
[InlineData("Live", true)] [InlineData("Live", true)]
[InlineData("BRB", false)] [InlineData("BRB", true)]
[InlineData("Chat", false)] [InlineData("Chat", true)]
[InlineData("Ending", false)] [InlineData("Ending", true)]
public void SceneCatalog_HasBackground_Only_For_Live(string name, bool expected) public void SceneCatalog_HasBackground_For_All_Canonical_Scenes(string name, bool expected)
{ {
Assert.Equal(expected, SceneCatalog.HasBackground(name)); Assert.Equal(expected, SceneCatalog.HasBackground(name));
} }
[Fact] [Fact]
public void EnforceBackgroundPolicy_Removes_Background_From_NonLive_Scenes() public void EnforceBackgroundPolicy_Sets_Flag_For_All_Scenes()
{ {
var chat = new Scene { Name = "Chat", HasBackground = true }; var chat = new Scene { Name = "Chat", HasBackground = false };
var live = new Scene { Name = "Live", HasBackground = true }; var live = new Scene { Name = "Live", HasBackground = false };
var chatBackground = MainViewModel.EnsureBackground(chat)!; var chatBackground = MainViewModel.EnsureBackground(chat)!;
var liveBackground = MainViewModel.EnsureBackground(live)!; var liveBackground = MainViewModel.EnsureBackground(live)!;
Assert.Contains(chat.Elements, e => ReferenceEquals(e, chatBackground));
MainViewModel.EnforceBackgroundPolicy(new[] { chat, live }); MainViewModel.EnforceBackgroundPolicy(new[] { chat, live });
Assert.False(chat.HasBackground); Assert.True(chat.HasBackground);
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackground: true }); Assert.Contains(chat.Elements, e => ReferenceEquals(e, chatBackground));
Assert.True(live.HasBackground); Assert.True(live.HasBackground);
Assert.Contains(live.Elements, e => ReferenceEquals(e, liveBackground)); Assert.Contains(live.Elements, e => ReferenceEquals(e, liveBackground));
} }
[Fact] [Fact]
public void EnforceBackgroundPolicy_Marks_Live_And_Clears_A_Pre_Policy_Database() public void EnforceBackgroundPolicy_Normalizes_All_Canonical_Scenes()
{ {
var starting = new Scene { Name = "Starting", HasBackground = true }; var starting = new Scene { Name = "Starting", HasBackground = false };
MainViewModel.EnsureBackground(starting); MainViewModel.EnsureBackground(starting);
var live = new Scene { Name = "Live", HasBackground = false }; var live = new Scene { Name = "Live", HasBackground = false };
MainViewModel.EnforceBackgroundPolicy(new[] { starting, live }); MainViewModel.EnforceBackgroundPolicy(new[] { starting, live });
Assert.False(starting.HasBackground); Assert.True(starting.HasBackground);
Assert.True(live.HasBackground); Assert.True(live.HasBackground);
} }
} }