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
## Branch
`main` — clean at `4b9a8e5` (Control Surface UX: pill toggle, background rename, per-scene properties).
`main` — clean, 0 warnings.
## What shipped this session
@@ -13,21 +13,31 @@
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
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)
- `Scene.UseDefaultBackground` (bool, default true) + `Scene.CustomBackgroundPath` (string?)
- Persisted as `BackgroundUseDefault_{sceneId}` / `BackgroundPath_{sceneId}` in Settings table
- Pill toggle ON → loads default asset from DB; OFF → loads custom file path
- Custom path preserved when toggled back ON (not cleared)
- Geometry mismatch: aspect ratio check ±5% of 16:9, shows expected 1920×1080
## Background layer rules
- Every scene has a Background layer at position 0 — undeletable (trash icon hidden, context menu "Remove Layer" hidden, `RemoveElement` returns early)
- `IsBackground` is derived from `SourceType.Background` via `Source.Type` setter — impossible to forget
- All five scenes have default images stored in Asset BLOBs in SQLite
- First run: pill ON for all five, shipped defaults load
- 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
```bash
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" 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
| Resource | ID |
@@ -42,7 +52,7 @@ taskkill.exe /F /IM ytLive.exe # if running
| Key prefix | `LCYT-` |
## 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
3. **Real snapshot generation** — replace placeholder colors with compositor-rendered thumbnails
4. **Transition dropdown UI** — add XAML control to pick transition type + duration
+10 -5
View File
@@ -608,17 +608,21 @@
<!-- Background properties -->
<StackPanel Margin="0,8,0,0"
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}"
IsChecked="{Binding UseDefaultBackground, Mode=TwoWay}"
Margin="0,0,0,8"/>
IsChecked="{Binding SelectedElement.UseDefaultBackground, Mode=TwoWay}"
Margin="8,0,0,0"/>
</StackPanel>
<Grid Margin="0,0,0,4"
Visibility="{Binding UseDefaultBackground, Converter={StaticResource InverseBoolToVis}}">
Visibility="{Binding SelectedElement.UseDefaultBackground, Converter={StaticResource InverseBoolToVis}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</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"
FontSize="11" VerticalAlignment="Center"/>
<Button Grid.Column="1" Content="Browse…" Command="{Binding BrowseBackgroundCommand}"
@@ -630,6 +634,7 @@
Visibility="{Binding BackgroundSizeMismatch, Converter={StaticResource BoolToVis}}"/>
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
-22
View File
@@ -15,8 +15,6 @@ public class Scene : INotifyPropertyChanged
private bool _hasSocialBar;
private bool _isStaged;
private bool _isLive;
private string? _customBackgroundPath;
private bool _useDefaultBackground = true;
public bool IsStaged
{
@@ -30,26 +28,6 @@ public class Scene : INotifyPropertyChanged
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
{
get => _name;
+2 -2
View File
@@ -47,6 +47,6 @@ public static class SceneCatalog
public static bool IsChat(string? name) => Is(name, Chat);
/// <summary>Only the Live scene carries the live desktop/game background.</summary>
public static bool HasBackground(string? name) => Is(name, Live);
/// <summary>Every scene has a background layer.</summary>
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 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;
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)
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))
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")
scene.UseDefaultBackground = false;
bg.UseDefaultBackground = false;
}
}
return scenes;
@@ -1025,12 +1034,15 @@ public class LayoutStore : IDisposable
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)
{
var pathKey = $"BackgroundPath_{scene.Id}";
if (!string.IsNullOrWhiteSpace(scene.CustomBackgroundPath))
UpsertSetting(pathKey, scene.CustomBackgroundPath);
var bg = scene.Elements.OfType<Source>().FirstOrDefault(s => s.IsBackground);
if (bg == null) continue;
var pathKey = $"BackgroundPath_{bg.Id}";
if (!string.IsNullOrWhiteSpace(bg.CustomBackgroundPath))
UpsertSetting(pathKey, bg.CustomBackgroundPath);
else
{
using var del = _connection.CreateCommand();
@@ -1039,8 +1051,8 @@ public class LayoutStore : IDisposable
del.ExecuteNonQuery();
}
var defaultKey = $"BackgroundUseDefault_{scene.Id}";
UpsertSetting(defaultKey, scene.UseDefaultBackground ? "1" : "0");
var defaultKey = $"BackgroundUseDefault_{bg.Id}";
UpsertSetting(defaultKey, bg.UseDefaultBackground ? "1" : "0");
}
tx.Commit();
+12 -5
View File
@@ -555,10 +555,17 @@
<Setter Property="Margin" Value="0,0,0,8"/>
</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) -->
<Style x:Key="PillToggle" TargetType="ToggleButton">
<Setter Property="Width" Value="48"/>
<Setter Property="Height" Value="24"/>
<Setter Property="Width" Value="36"/>
<Setter Property="Height" Value="18"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Background" Value="#ef4444"/>
<Setter Property="Template">
@@ -566,8 +573,8 @@
<ControlTemplate TargetType="ToggleButton">
<Grid>
<Border x:Name="Track" Background="{TemplateBinding Background}"
CornerRadius="12" SnapsToDevicePixels="True">
<Ellipse x:Name="Thumb" Width="18" Height="18" Fill="White"
CornerRadius="9" SnapsToDevicePixels="True">
<Ellipse x:Name="Thumb" Width="12" Height="12" Fill="White"
HorizontalAlignment="Left" Margin="3,0,0,0">
<Ellipse.Effect>
<DropShadowEffect BlurRadius="3" ShadowDepth="1" Opacity="0.35"/>
@@ -586,7 +593,7 @@
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ThumbTransform"
Storyboard.TargetProperty="X"
To="24" Duration="0:0:0.15">
To="18" Duration="0:0:0.15">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
+26 -48
View File
@@ -206,8 +206,6 @@ public class MainViewModel : ViewModelBase
OnPropertyChanged(nameof(CanAddWebcamToStagedScene));
OnPropertyChanged(nameof(CanShowWebcamInStagedScene));
OnPropertyChanged(nameof(IsCountdownAvailable));
OnPropertyChanged(nameof(UseDefaultBackground));
OnPropertyChanged(nameof(CustomBackgroundPath));
UpdateActiveBackground();
UpdateBackgroundImage();
RefreshSnapshotsAsync();
@@ -274,14 +272,28 @@ public class MainViewModel : ViewModelBase
get => _selectedElement;
set
{
if (_selectedElement != null)
_selectedElement.PropertyChanged -= OnSelectedElementPropertyChanged;
if (SetProperty(ref _selectedElement, value))
{
OnPropertyChanged(nameof(IsWebcamSelected));
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>
public bool CanAddWebcamToStagedScene => StagedScene?.WebcamConfig == null;
@@ -932,15 +944,15 @@ public class MainViewModel : ViewModelBase
var background = scene?.Elements.OfType<Source>().FirstOrDefault(s => s.IsBackground);
BackgroundImage = background?.DisplaySource;
if (scene != null && !scene.UseDefaultBackground
&& !string.IsNullOrWhiteSpace(scene.CustomBackgroundPath)
&& System.IO.File.Exists(scene.CustomBackgroundPath))
if (background != null && !background.UseDefaultBackground
&& !string.IsNullOrWhiteSpace(background.CustomBackgroundPath)
&& System.IO.File.Exists(background.CustomBackgroundPath))
{
try
{
var bmi = new System.Windows.Media.Imaging.BitmapImage();
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.EndInit();
bmi.Freeze();
@@ -988,15 +1000,16 @@ public class MainViewModel : ViewModelBase
private static System.Windows.Media.Imaging.BitmapSource? LoadBackgroundImage(Scene scene)
{
if (!scene.UseDefaultBackground
&& !string.IsNullOrWhiteSpace(scene.CustomBackgroundPath)
&& System.IO.File.Exists(scene.CustomBackgroundPath))
var bg = scene.Elements.OfType<Source>().FirstOrDefault(s => s.Type == SourceType.Background);
if (bg != null && !bg.UseDefaultBackground
&& !string.IsNullOrWhiteSpace(bg.CustomBackgroundPath)
&& System.IO.File.Exists(bg.CustomBackgroundPath))
{
try
{
var bitmap = new System.Windows.Media.Imaging.BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(scene.CustomBackgroundPath);
bitmap.UriSource = new Uri(bg.CustomBackgroundPath);
bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
@@ -1004,7 +1017,6 @@ public class MainViewModel : ViewModelBase
}
catch { /* fall through to AssetId */ }
}
var bg = scene.Elements.OfType<Source>().FirstOrDefault(s => s.Type == SourceType.Background);
if (bg != null && !string.IsNullOrWhiteSpace(bg.AssetId))
return ImageCache.Get(bg.AssetId);
return null;
@@ -1848,20 +1860,11 @@ public class MainViewModel : ViewModelBase
return background;
}
// The background belongs to the Live scene alone. Runs after every layout
// 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.
// Every scene has a background layer. Normalize the flag by scene name.
internal static void EnforceBackgroundPolicy(IEnumerable<Scene> scenes)
{
foreach (var scene in scenes)
{
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()
@@ -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 BrowseBackground()
@@ -2037,8 +2015,8 @@ public class MainViewModel : ViewModelBase
};
if (dlg.ShowDialog() != true) return;
scene.CustomBackgroundPath = dlg.FileName;
scene.UseDefaultBackground = false;
bg.CustomBackgroundPath = dlg.FileName;
bg.UseDefaultBackground = false;
try
{
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
- 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
- `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)
{
var scene = new Scene { Name = name };
Assert.True(scene.UseDefaultBackground,
$"{name}: Scene must default to UseDefaultBackground = true");
var bg = new Source { Name = "Background", Type = SourceType.Background };
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]
[InlineData("Starting", false)]
[InlineData("Starting", true)]
[InlineData("Live", true)]
[InlineData("BRB", false)]
[InlineData("Chat", false)]
[InlineData("Ending", false)]
public void SceneCatalog_HasBackground_Only_For_Live(string name, bool expected)
[InlineData("BRB", true)]
[InlineData("Chat", true)]
[InlineData("Ending", true)]
public void SceneCatalog_HasBackground_For_All_Canonical_Scenes(string name, bool expected)
{
Assert.Equal(expected, SceneCatalog.HasBackground(name));
}
[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 live = new Scene { Name = "Live", HasBackground = true };
var chat = new Scene { Name = "Chat", HasBackground = false };
var live = new Scene { Name = "Live", HasBackground = false };
var chatBackground = MainViewModel.EnsureBackground(chat)!;
var liveBackground = MainViewModel.EnsureBackground(live)!;
Assert.Contains(chat.Elements, e => ReferenceEquals(e, chatBackground));
MainViewModel.EnforceBackgroundPolicy(new[] { chat, live });
Assert.False(chat.HasBackground);
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackground: true });
Assert.True(chat.HasBackground);
Assert.Contains(chat.Elements, e => ReferenceEquals(e, chatBackground));
Assert.True(live.HasBackground);
Assert.Contains(live.Elements, e => ReferenceEquals(e, liveBackground));
}
[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);
var live = new Scene { Name = "Live", HasBackground = false };
MainViewModel.EnforceBackgroundPolicy(new[] { starting, live });
Assert.False(starting.HasBackground);
Assert.True(starting.HasBackground);
Assert.True(live.HasBackground);
}
}