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:
+20
-10
@@ -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
|
||||||
|
|||||||
+25
-20
@@ -608,26 +608,31 @@
|
|||||||
<!-- 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}"/>
|
||||||
<ToggleButton Style="{StaticResource PillToggle}"
|
<StackPanel Margin="4,0,0,0">
|
||||||
IsChecked="{Binding UseDefaultBackground, Mode=TwoWay}"
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||||
Margin="0,0,0,8"/>
|
<TextBlock Text="Use default background:" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center"/>
|
||||||
<Grid Margin="0,0,0,4"
|
<ToggleButton Style="{StaticResource PillToggle}"
|
||||||
Visibility="{Binding UseDefaultBackground, Converter={StaticResource InverseBoolToVis}}">
|
IsChecked="{Binding SelectedElement.UseDefaultBackground, Mode=TwoWay}"
|
||||||
<Grid.ColumnDefinitions>
|
Margin="8,0,0,0"/>
|
||||||
<ColumnDefinition Width="*"/>
|
</StackPanel>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<Grid Margin="0,0,0,4"
|
||||||
</Grid.ColumnDefinitions>
|
Visibility="{Binding SelectedElement.UseDefaultBackground, Converter={StaticResource InverseBoolToVis}}">
|
||||||
<TextBox Grid.Column="0" Text="{Binding CustomBackgroundPath, Mode=OneWay}"
|
<Grid.ColumnDefinitions>
|
||||||
IsReadOnly="True" Background="#0f3460" Foreground="#a0a0b0"
|
<ColumnDefinition Width="*"/>
|
||||||
FontSize="11" VerticalAlignment="Center"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<Button Grid.Column="1" Content="Browse…" Command="{Binding BrowseBackgroundCommand}"
|
</Grid.ColumnDefinitions>
|
||||||
Style="{StaticResource YtButtonSecondary}" Margin="4,0,0,0" Padding="8,2"/>
|
<TextBox Grid.Column="0" Text="{Binding SelectedElement.CustomBackgroundPath, Mode=OneWay}"
|
||||||
</Grid>
|
IsReadOnly="True" Background="#0f3460" Foreground="#a0a0b0"
|
||||||
<TextBlock Text="{Binding BackgroundSizeMismatchText}"
|
FontSize="11" VerticalAlignment="Center"/>
|
||||||
Foreground="#ef4444" FontSize="11" TextWrapping="Wrap"
|
<Button Grid.Column="1" Content="Browse…" Command="{Binding BrowseBackgroundCommand}"
|
||||||
Margin="0,4,0,0"
|
Style="{StaticResource YtButtonSecondary}" Margin="4,0,0,0" Padding="8,2"/>
|
||||||
Visibility="{Binding BackgroundSizeMismatch, Converter={StaticResource BoolToVis}}"/>
|
</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>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|
||||||
|
|||||||
+24
-12
@@ -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 (!string.IsNullOrWhiteSpace(path))
|
if (bg != null)
|
||||||
scene.CustomBackgroundPath = path;
|
{
|
||||||
|
// 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))
|
||||||
|
bg.CustomBackgroundPath = path;
|
||||||
|
|
||||||
var useDefault = GetSetting($"BackgroundUseDefault_{scene.Id}");
|
var useDefault = GetSetting($"BackgroundUseDefault_{bg.Id}");
|
||||||
if (useDefault == "0")
|
if (string.IsNullOrWhiteSpace(useDefault))
|
||||||
scene.UseDefaultBackground = false;
|
useDefault = GetSetting($"BackgroundUseDefault_{scene.Id}");
|
||||||
|
if (useDefault == "0")
|
||||||
|
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
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user