Control Surface UX: pill toggle, background rename, per-scene properties
- Sliding pill toggle (green ON / red OFF) replaces checkbox - Geometry mismatch error for custom backgrounds - Backdrop -> Background: C# rename, SQL column rename with migration - Source.IsBackground: new -> override (fixes WPF binding) - Legacy 'Backdrop' display name migrated to 'Background' - Per-scene UseDefaultBackground + CustomBackgroundPath - Asset files renamed backdrop -> background - StagedScene/LiveScene split, thumbnail strip, edit gating - TransitionService (Cut/Fade/Move) - Context menu: Capture Desktop, Refresh Desktop - Resolution badge removed, ON-AIR status light added
This commit is contained in:
+117
-27
@@ -47,7 +47,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,
|
||||
HasBackground INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
""",
|
||||
@@ -77,7 +77,7 @@ public class LayoutStore : IDisposable
|
||||
DeviceId TEXT,
|
||||
ClipShape TEXT NOT NULL DEFAULT 'Traditional',
|
||||
IsMirrored INTEGER NOT NULL DEFAULT 0,
|
||||
IsBackdrop INTEGER NOT NULL DEFAULT 0,
|
||||
IsBackground INTEGER NOT NULL DEFAULT 0,
|
||||
CaptureKey TEXT,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
@@ -198,10 +198,16 @@ public class LayoutStore : IDisposable
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
if (!columns.Contains("IsBackdrop"))
|
||||
if (columns.Contains("IsBackdrop"))
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "ALTER TABLE Source ADD COLUMN IsBackdrop INTEGER NOT NULL DEFAULT 0;";
|
||||
cmd.CommandText = "ALTER TABLE Source RENAME COLUMN IsBackdrop TO IsBackground;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
else if (!columns.Contains("IsBackground"))
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "ALTER TABLE Source ADD COLUMN IsBackground INTEGER NOT NULL DEFAULT 0;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
@@ -267,6 +273,13 @@ public class LayoutStore : IDisposable
|
||||
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ShowDesktop INTEGER NOT NULL DEFAULT 0;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// Rename legacy "Backdrop" display name to "Background" in existing rows
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "UPDATE Source SET Name = 'Background' WHERE Name = 'Backdrop';";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
// v3 → v4: WebcamSceneConfig gains RectWidth/RectHeight (the pre-Round rect,
|
||||
@@ -374,10 +387,10 @@ public class LayoutStore : IDisposable
|
||||
tx.Commit();
|
||||
}
|
||||
|
||||
// v5 → v6: Scene gains HasBackdrop. Added columns default to true; the
|
||||
// v5 → v6: Scene gains HasBackground. 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
|
||||
// non-Live canonical scene off and drops their background sources — the fix for
|
||||
// DBs saved before the Live-only policy. MainViewModel.EnforceBackgroundPolicy
|
||||
// re-runs the same normalization on every load for DBs that miss the backfill.
|
||||
private void MigrateSceneTable()
|
||||
{
|
||||
@@ -390,26 +403,36 @@ public class LayoutStore : IDisposable
|
||||
columns.Add(reader.GetString(1));
|
||||
}
|
||||
|
||||
if (columns.Contains("HasBackdrop")) return;
|
||||
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
if (columns.Contains("HasBackdrop"))
|
||||
{
|
||||
cmd.CommandText = "ALTER TABLE Scene ADD COLUMN HasBackdrop INTEGER NOT NULL DEFAULT 1;";
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "ALTER TABLE Scene RENAME COLUMN HasBackdrop TO HasBackground;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
else if (columns.Contains("HasBackground"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!columns.Contains("HasBackground"))
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "ALTER TABLE Scene ADD COLUMN HasBackground INTEGER NOT NULL DEFAULT 1;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
const string noBackdrop = "('starting', 'brb', 'chat', 'ending')";
|
||||
const string noBackground = "('starting', 'brb', 'chat', 'ending')";
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = $"UPDATE Scene SET HasBackdrop = 0 WHERE LOWER(TRIM(Name)) IN {noBackdrop};";
|
||||
cmd.CommandText = $"UPDATE Scene SET HasBackground = 0 WHERE LOWER(TRIM(Name)) IN {noBackground};";
|
||||
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});
|
||||
WHERE IsBackground = 1 AND SceneId IN
|
||||
(SELECT Id FROM Scene WHERE LOWER(TRIM(Name)) IN {noBackground});
|
||||
""";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
@@ -486,7 +509,7 @@ public class LayoutStore : IDisposable
|
||||
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "SELECT Id, Name, IsHidden, IsChatScene, HasBackdrop, HasSocialBar FROM Scene ORDER BY SortOrder";
|
||||
cmd.CommandText = "SELECT Id, Name, IsHidden, IsChatScene, HasBackground, HasSocialBar FROM Scene ORDER BY SortOrder";
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
@@ -496,7 +519,7 @@ public class LayoutStore : IDisposable
|
||||
Name = reader.GetString(1),
|
||||
IsHidden = reader.GetInt32(2) != 0,
|
||||
IsChatScene = reader.GetInt32(3) != 0,
|
||||
HasBackdrop = reader.GetInt32(4) != 0,
|
||||
HasBackground = reader.GetInt32(4) != 0,
|
||||
HasSocialBar = reader.FieldCount > 5 && !reader.IsDBNull(5) && reader.GetInt32(5) != 0,
|
||||
});
|
||||
}
|
||||
@@ -566,7 +589,7 @@ public class LayoutStore : IDisposable
|
||||
cmd.CommandText = """
|
||||
SELECT Id, SceneId, AssetId, Type, Name, IsEnabled,
|
||||
X, Y, Width, Height, Opacity, MonitorIndex, ClipShape, IsMirrored,
|
||||
IsBackdrop, CaptureKey, ShowDesktop,
|
||||
IsBackground, CaptureKey, ShowDesktop,
|
||||
ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages,
|
||||
ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor
|
||||
FROM Source ORDER BY SortOrder
|
||||
@@ -590,7 +613,7 @@ 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,
|
||||
IsBackground = reader.GetInt32(14) != 0,
|
||||
CaptureKey = reader.IsDBNull(15) ? null : reader.GetString(15),
|
||||
ShowDesktop = reader.GetInt32(16) != 0,
|
||||
ChatFontSize = reader.IsDBNull(17) ? 16 : reader.GetDouble(17),
|
||||
@@ -653,6 +676,14 @@ public class LayoutStore : IDisposable
|
||||
if (configsByScene.TryGetValue(scene.Id, out var configs))
|
||||
foreach (var config in configs)
|
||||
scene.Elements.Add(config);
|
||||
|
||||
var path = GetSetting($"BackgroundPath_{scene.Id}");
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
scene.CustomBackgroundPath = path;
|
||||
|
||||
var useDefault = GetSetting($"BackgroundUseDefault_{scene.Id}");
|
||||
if (useDefault == "0")
|
||||
scene.UseDefaultBackground = false;
|
||||
}
|
||||
|
||||
return scenes;
|
||||
@@ -760,15 +791,15 @@ public class LayoutStore : IDisposable
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = """
|
||||
INSERT INTO Scene (Id, Name, IsHidden, IsChatScene, HasBackdrop, HasSocialBar, SortOrder)
|
||||
VALUES ($id, $name, $isHidden, $isChat, $hasBackdrop, $hasSocialBar, $sort)
|
||||
INSERT INTO Scene (Id, Name, IsHidden, IsChatScene, HasBackground, HasSocialBar, SortOrder)
|
||||
VALUES ($id, $name, $isHidden, $isChat, $hasBackground, $hasSocialBar, $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 backgroundP = cmd.Parameters.Add("$hasBackground", SqliteType.Integer);
|
||||
var socialBarP = cmd.Parameters.Add("$hasSocialBar", SqliteType.Integer);
|
||||
var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer);
|
||||
|
||||
@@ -779,7 +810,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;
|
||||
backgroundP.Value = scene.HasBackground ? 1 : 0;
|
||||
socialBarP.Value = scene.HasSocialBar ? 1 : 0;
|
||||
sortP.Value = sort++;
|
||||
cmd.ExecuteNonQuery();
|
||||
@@ -791,12 +822,12 @@ public class LayoutStore : IDisposable
|
||||
cmd.CommandText = """
|
||||
INSERT INTO Source (Id, SceneId, AssetId, Type, Name, IsEnabled,
|
||||
X, Y, Width, Height, Opacity, MonitorIndex,
|
||||
ClipShape, IsMirrored, IsBackdrop, CaptureKey, ShowDesktop, SortOrder,
|
||||
ClipShape, IsMirrored, IsBackground, CaptureKey, ShowDesktop, SortOrder,
|
||||
ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages,
|
||||
ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor)
|
||||
VALUES ($id, $sceneId, $assetId, $type, $name, $isEnabled,
|
||||
$x, $y, $w, $h, $opacity, $monitor,
|
||||
$clip, $mirrored, $isBackdrop, $captureKey, $showDesktop, $sort,
|
||||
$clip, $mirrored, $isBackground, $captureKey, $showDesktop, $sort,
|
||||
$chatFontSize, $chatFontColor, $chatBgColor, $chatMaxMsgs,
|
||||
$chatTimestamps, $chatBadges, $chatMemberColor, $chatSuperChatColor)
|
||||
""";
|
||||
@@ -815,7 +846,7 @@ 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 isBackgroundP = cmd.Parameters.Add("$isBackground", SqliteType.Integer);
|
||||
var captureKeyP = cmd.Parameters.Add("$captureKey", SqliteType.Text);
|
||||
var showDesktopP = cmd.Parameters.Add("$showDesktop", SqliteType.Integer);
|
||||
var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer);
|
||||
@@ -848,7 +879,7 @@ 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;
|
||||
isBackgroundP.Value = source.IsBackground ? 1 : 0;
|
||||
captureKeyP.Value = (object?)source.CaptureKey ?? DBNull.Value;
|
||||
showDesktopP.Value = source.ShowDesktop ? 1 : 0;
|
||||
sortP.Value = sort++;
|
||||
@@ -994,6 +1025,24 @@ public class LayoutStore : IDisposable
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// Save custom background paths and use-default flags
|
||||
foreach (var scene in scenes)
|
||||
{
|
||||
var pathKey = $"BackgroundPath_{scene.Id}";
|
||||
if (!string.IsNullOrWhiteSpace(scene.CustomBackgroundPath))
|
||||
UpsertSetting(pathKey, scene.CustomBackgroundPath);
|
||||
else
|
||||
{
|
||||
using var del = _connection.CreateCommand();
|
||||
del.CommandText = "DELETE FROM Settings WHERE Key = $key;";
|
||||
del.Parameters.AddWithValue("$key", pathKey);
|
||||
del.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
var defaultKey = $"BackgroundUseDefault_{scene.Id}";
|
||||
UpsertSetting(defaultKey, scene.UseDefaultBackground ? "1" : "0");
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
}
|
||||
|
||||
@@ -1097,6 +1146,47 @@ public class LayoutStore : IDisposable
|
||||
insert.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public (string TransitionType, double DurationMs) LoadTransitionSettings()
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT Value FROM Settings WHERE Key = 'TransitionType';";
|
||||
var type = cmd.ExecuteScalar() as string;
|
||||
|
||||
using var cmd2 = _connection.CreateCommand();
|
||||
cmd2.CommandText = "SELECT Value FROM Settings WHERE Key = 'TransitionDurationMs';";
|
||||
var dur = cmd2.ExecuteScalar() as string;
|
||||
|
||||
return (type ?? "Fade", dur != null && double.TryParse(dur, out var d) ? d : 300);
|
||||
}
|
||||
|
||||
public void SaveTransitionSettings(string transitionType, double durationMs)
|
||||
{
|
||||
UpsertSetting("TransitionType", transitionType);
|
||||
UpsertSetting("TransitionDurationMs", durationMs.ToString("F0"));
|
||||
}
|
||||
|
||||
private string? GetSetting(string key)
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT Value FROM Settings WHERE Key = $key;";
|
||||
cmd.Parameters.AddWithValue("$key", key);
|
||||
return cmd.ExecuteScalar() as string;
|
||||
}
|
||||
|
||||
private void UpsertSetting(string key, string value)
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "DELETE FROM Settings WHERE Key = $key;";
|
||||
cmd.Parameters.AddWithValue("$key", key);
|
||||
cmd.ExecuteNonQuery();
|
||||
|
||||
using var insert = _connection.CreateCommand();
|
||||
insert.CommandText = "INSERT INTO Settings (Key, Value) VALUES ($key, $value);";
|
||||
insert.Parameters.AddWithValue("$key", key);
|
||||
insert.Parameters.AddWithValue("$value", value);
|
||||
insert.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_connection.Dispose();
|
||||
|
||||
Reference in New Issue
Block a user