Screen backdrop (schema v5/v6) + five-scene catalog + webcam polish: live desktop/game capture as a permanent non-deletable bottom layer (Source.IsBackdrop), auto full-screen game detection (Win32FullScreenDetector) else primary display, GraphicsCapturePicker re-designation, refcounted shared ScreenCaptureManager, Live-only backdrop by policy (HasBackdrop + v5-v6 backfill + EnforceBackdropPolicy, checkbox gone), SceneCatalog (Starting/Live/BRB/Chat/Ending) with + button re-adding missing scenes, webcam mid-session transparent-container fix (GetPreviewBitmap propagation), Chat half-screen-area cap, 'Add Webcam' always opens the picker (SwapWebcamIdentityAsync, no silent resurrect), WindowsRuntimeMarshal frame-read + 5s-throttled errors, docs updated, tests (65 passing)

This commit is contained in:
2026-08-07 14:15:50 -07:00
parent e037ba027b
commit ad9b3e1e48
27 changed files with 2232 additions and 116 deletions
+76 -7
View File
@@ -40,6 +40,7 @@ public class LayoutStore : IDisposable
Name TEXT NOT NULL,
IsHidden INTEGER NOT NULL DEFAULT 0,
IsChatScene INTEGER NOT NULL DEFAULT 0,
HasBackdrop INTEGER NOT NULL DEFAULT 1,
SortOrder INTEGER NOT NULL DEFAULT 0
);
""",
@@ -69,6 +70,8 @@ public class LayoutStore : IDisposable
DeviceId TEXT,
ClipShape TEXT NOT NULL DEFAULT 'Traditional',
IsMirrored INTEGER NOT NULL DEFAULT 0,
IsBackdrop INTEGER NOT NULL DEFAULT 0,
CaptureKey TEXT,
SortOrder INTEGER NOT NULL DEFAULT 0
);
""",
@@ -110,11 +113,12 @@ public class LayoutStore : IDisposable
}
MigrateSourceTable();
MigrateWebcamConfigTable();
MigrateSceneTable();
if (GetUserVersion() < 3)
MigrateToV3();
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = "PRAGMA user_version = 4;";
cmd.CommandText = "PRAGMA user_version = 6;";
cmd.ExecuteNonQuery();
}
}
@@ -153,6 +157,20 @@ public class LayoutStore : IDisposable
cmd.CommandText = "ALTER TABLE Source ADD COLUMN IsMirrored INTEGER NOT NULL DEFAULT 0;";
cmd.ExecuteNonQuery();
}
if (!columns.Contains("IsBackdrop"))
{
using var cmd = _connection.CreateCommand();
cmd.CommandText = "ALTER TABLE Source ADD COLUMN IsBackdrop INTEGER NOT NULL DEFAULT 0;";
cmd.ExecuteNonQuery();
}
if (!columns.Contains("CaptureKey"))
{
using var cmd = _connection.CreateCommand();
cmd.CommandText = "ALTER TABLE Source ADD COLUMN CaptureKey TEXT;";
cmd.ExecuteNonQuery();
}
}
// v3 → v4: WebcamSceneConfig gains RectWidth/RectHeight (the pre-Round rect,
@@ -260,6 +278,47 @@ public class LayoutStore : IDisposable
tx.Commit();
}
// v5 → v6: Scene gains HasBackdrop. Added columns default to true; the
// ONE-TIME backfill (runs only when the column is first added) turns every
// non-Live canonical scene off and drops their backdrop sources — the fix for
// DBs saved before the Live-only policy. MainViewModel.EnforceBackdropPolicy
// re-runs the same normalization on every load for DBs that miss the backfill.
private void MigrateSceneTable()
{
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = "PRAGMA table_info(Scene);";
using var reader = cmd.ExecuteReader();
while (reader.Read())
columns.Add(reader.GetString(1));
}
if (columns.Contains("HasBackdrop")) return;
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = "ALTER TABLE Scene ADD COLUMN HasBackdrop INTEGER NOT NULL DEFAULT 1;";
cmd.ExecuteNonQuery();
}
const string noBackdrop = "('starting', 'brb', 'chat', 'ending')";
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = $"UPDATE Scene SET HasBackdrop = 0 WHERE LOWER(TRIM(Name)) IN {noBackdrop};";
cmd.ExecuteNonQuery();
}
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = $"""
DELETE FROM Source
WHERE IsBackdrop = 1 AND SceneId IN
(SELECT Id FROM Scene WHERE LOWER(TRIM(Name)) IN {noBackdrop});
""";
cmd.ExecuteNonQuery();
}
}
public List<Scene> Load()
{
Webcam = null;
@@ -269,7 +328,7 @@ public class LayoutStore : IDisposable
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = "SELECT Id, Name, IsHidden, IsChatScene FROM Scene ORDER BY SortOrder";
cmd.CommandText = "SELECT Id, Name, IsHidden, IsChatScene, HasBackdrop FROM Scene ORDER BY SortOrder";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
@@ -279,6 +338,7 @@ public class LayoutStore : IDisposable
Name = reader.GetString(1),
IsHidden = reader.GetInt32(2) != 0,
IsChatScene = reader.GetInt32(3) != 0,
HasBackdrop = reader.GetInt32(4) != 0,
});
}
}
@@ -302,7 +362,8 @@ public class LayoutStore : IDisposable
{
cmd.CommandText = """
SELECT Id, SceneId, AssetId, Type, Name, IsEnabled,
X, Y, Width, Height, Opacity, MonitorIndex, ClipShape, IsMirrored
X, Y, Width, Height, Opacity, MonitorIndex, ClipShape, IsMirrored,
IsBackdrop, CaptureKey
FROM Source ORDER BY SortOrder
""";
using var reader = cmd.ExecuteReader();
@@ -324,6 +385,8 @@ public class LayoutStore : IDisposable
MonitorIndex = reader.IsDBNull(11) ? null : reader.GetInt32(11),
ClipShape = Enum.TryParse<ClipShape>(reader.GetString(12), out var clip) ? clip : ClipShape.Traditional,
IsMirrored = reader.GetInt32(13) != 0,
IsBackdrop = reader.GetInt32(14) != 0,
CaptureKey = reader.IsDBNull(15) ? null : reader.GetString(15),
};
if (!sourcesByScene.TryGetValue(sceneId, out var list))
sourcesByScene[sceneId] = list = new List<Source>();
@@ -412,14 +475,15 @@ public class LayoutStore : IDisposable
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = """
INSERT INTO Scene (Id, Name, IsHidden, IsChatScene, SortOrder)
VALUES ($id, $name, $isHidden, $isChat, $sort)
INSERT INTO Scene (Id, Name, IsHidden, IsChatScene, HasBackdrop, SortOrder)
VALUES ($id, $name, $isHidden, $isChat, $hasBackdrop, $sort)
""";
cmd.Transaction = tx;
var idP = cmd.Parameters.Add("$id", SqliteType.Text);
var nameP = cmd.Parameters.Add("$name", SqliteType.Text);
var hiddenP = cmd.Parameters.Add("$isHidden", SqliteType.Integer);
var chatP = cmd.Parameters.Add("$isChat", SqliteType.Integer);
var backdropP = cmd.Parameters.Add("$hasBackdrop", SqliteType.Integer);
var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer);
var sort = 0;
@@ -429,6 +493,7 @@ public class LayoutStore : IDisposable
nameP.Value = scene.Name;
hiddenP.Value = scene.IsHidden ? 1 : 0;
chatP.Value = scene.IsChatScene ? 1 : 0;
backdropP.Value = scene.HasBackdrop ? 1 : 0;
sortP.Value = sort++;
cmd.ExecuteNonQuery();
}
@@ -439,10 +504,10 @@ public class LayoutStore : IDisposable
cmd.CommandText = """
INSERT INTO Source (Id, SceneId, AssetId, Type, Name, IsEnabled,
X, Y, Width, Height, Opacity, MonitorIndex,
ClipShape, IsMirrored, SortOrder)
ClipShape, IsMirrored, IsBackdrop, CaptureKey, SortOrder)
VALUES ($id, $sceneId, $assetId, $type, $name, $isEnabled,
$x, $y, $w, $h, $opacity, $monitor,
$clip, $mirrored, $sort)
$clip, $mirrored, $isBackdrop, $captureKey, $sort)
""";
cmd.Transaction = tx;
var idP = cmd.Parameters.Add("$id", SqliteType.Text);
@@ -459,6 +524,8 @@ public class LayoutStore : IDisposable
var monitorP = cmd.Parameters.Add("$monitor", SqliteType.Integer);
var clipP = cmd.Parameters.Add("$clip", SqliteType.Text);
var mirroredP = cmd.Parameters.Add("$mirrored", SqliteType.Integer);
var isBackdropP = cmd.Parameters.Add("$isBackdrop", SqliteType.Integer);
var captureKeyP = cmd.Parameters.Add("$captureKey", SqliteType.Text);
var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer);
foreach (var scene in scenes)
@@ -481,6 +548,8 @@ public class LayoutStore : IDisposable
monitorP.Value = (object?)source.MonitorIndex ?? DBNull.Value;
clipP.Value = source.ClipShape.ToString();
mirroredP.Value = source.IsMirrored ? 1 : 0;
isBackdropP.Value = source.IsBackdrop ? 1 : 0;
captureKeyP.Value = (object?)source.CaptureKey ?? DBNull.Value;
sortP.Value = sort++;
cmd.ExecuteNonQuery();
}