diff --git a/Services/LayoutStore.Snapshots.cs b/Services/LayoutStore.Snapshots.cs
index b6042ff..403b5bf 100644
--- a/Services/LayoutStore.Snapshots.cs
+++ b/Services/LayoutStore.Snapshots.cs
@@ -78,6 +78,120 @@ public partial class LayoutStore : IDisposable
CaptureDefaults();
}
+ /// Restores one existing scene by its current ID. Unknown scenes have no branded default
+ /// and are left untouched.
+ public bool RestoreDefaultScene(string sceneId)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(sceneId);
+
+ string? defaultSceneId;
+ using (var find = _connection.CreateCommand())
+ {
+ find.CommandText = """
+ SELECT Id FROM DefaultScene
+ WHERE Name = (SELECT Name FROM Scene WHERE Id = $sceneId) COLLATE NOCASE
+ LIMIT 1;
+ """;
+ find.Parameters.AddWithValue("$sceneId", sceneId);
+ defaultSceneId = find.ExecuteScalar() as string;
+ }
+ if (defaultSceneId == null) return false;
+
+ var columnsByTable = SnapshotTables.ToDictionary(table => table, ReadColumnNames);
+ var webcamId = GetCurrentWebcamId();
+ using var tx = _connection.BeginTransaction();
+
+ using (var clearBackgroundSettings = _connection.CreateCommand())
+ {
+ clearBackgroundSettings.Transaction = tx;
+ clearBackgroundSettings.CommandText = """
+ DELETE FROM Settings
+ WHERE Key IN (
+ SELECT 'BackgroundPath_' || Id FROM Source WHERE SceneId = $sceneId
+ UNION SELECT 'BackgroundUseDefault_' || Id FROM Source WHERE SceneId = $sceneId
+ UNION SELECT 'BackgroundPath_' || $sceneId
+ UNION SELECT 'BackgroundUseDefault_' || $sceneId
+ );
+ """;
+ clearBackgroundSettings.Parameters.AddWithValue("$sceneId", sceneId);
+ clearBackgroundSettings.ExecuteNonQuery();
+ }
+
+ foreach (var table in new[] { "WebcamSceneConfig", "Source" })
+ {
+ using var delete = _connection.CreateCommand();
+ delete.Transaction = tx;
+ delete.CommandText = $"DELETE FROM {table} WHERE SceneId = $sceneId;";
+ delete.Parameters.AddWithValue("$sceneId", sceneId);
+ delete.ExecuteNonQuery();
+ }
+
+ var sceneColumns = columnsByTable["Scene"]
+ .Where(column => column != "Id" && column != "SortOrder").ToArray();
+ var assignments = string.Join(", ", sceneColumns.Select(column =>
+ $"{QuoteIdentifier(column)} = (SELECT {QuoteIdentifier(column)} FROM DefaultScene WHERE Id = $defaultSceneId)"));
+ using (var updateScene = _connection.CreateCommand())
+ {
+ updateScene.Transaction = tx;
+ updateScene.CommandText = $"UPDATE Scene SET {assignments} WHERE Id = $sceneId;";
+ updateScene.Parameters.AddWithValue("$sceneId", sceneId);
+ updateScene.Parameters.AddWithValue("$defaultSceneId", defaultSceneId);
+ updateScene.ExecuteNonQuery();
+ }
+
+ CopyDefaultSceneRows("Source", sceneId, defaultSceneId, null, tx, columnsByTable["Source"]);
+ if (webcamId != null)
+ CopyDefaultSceneRows("WebcamSceneConfig", sceneId, defaultSceneId, webcamId, tx,
+ columnsByTable["WebcamSceneConfig"]);
+
+ using (var copyBackgroundSettings = _connection.CreateCommand())
+ {
+ copyBackgroundSettings.Transaction = tx;
+ copyBackgroundSettings.CommandText = """
+ INSERT INTO Settings (Key, Value)
+ SELECT Key, Value FROM DefaultLayoutSetting
+ WHERE Key IN (
+ SELECT 'BackgroundPath_' || Id FROM DefaultSource WHERE SceneId = $defaultSceneId
+ UNION SELECT 'BackgroundUseDefault_' || Id FROM DefaultSource WHERE SceneId = $defaultSceneId
+ UNION SELECT 'BackgroundPath_' || $defaultSceneId
+ UNION SELECT 'BackgroundUseDefault_' || $defaultSceneId
+ );
+ """;
+ copyBackgroundSettings.Parameters.AddWithValue("$defaultSceneId", defaultSceneId);
+ copyBackgroundSettings.ExecuteNonQuery();
+ }
+
+ tx.Commit();
+ return true;
+ }
+
+ private string? GetCurrentWebcamId()
+ {
+ using var cmd = _connection.CreateCommand();
+ cmd.CommandText = "SELECT Id FROM Webcam LIMIT 1;";
+ return cmd.ExecuteScalar() as string;
+ }
+
+ private void CopyDefaultSceneRows(string table, string sceneId, string defaultSceneId,
+ string? webcamId, SqliteTransaction tx, IReadOnlyList columns)
+ {
+ var columnList = string.Join(", ", columns.Select(QuoteIdentifier));
+ var values = string.Join(", ", columns.Select(column => column switch
+ {
+ "SceneId" => "$sceneId",
+ "WebcamId" when webcamId != null => "$webcamId",
+ _ => QuoteIdentifier(column),
+ }));
+
+ using var copy = _connection.CreateCommand();
+ copy.Transaction = tx;
+ copy.CommandText = $"INSERT INTO {table} ({columnList}) SELECT {values} FROM Default{table} WHERE SceneId = $defaultSceneId;";
+ copy.Parameters.AddWithValue("$sceneId", sceneId);
+ copy.Parameters.AddWithValue("$defaultSceneId", defaultSceneId);
+ if (webcamId != null) copy.Parameters.AddWithValue("$webcamId", webcamId);
+ copy.ExecuteNonQuery();
+ }
+
private void CaptureDefaults()
{
var columnsByTable = SnapshotTables.ToDictionary(table => table, ReadColumnNames);
diff --git a/ytLive.Tests/LayoutStoreSnapshotTests.cs b/ytLive.Tests/LayoutStoreSnapshotTests.cs
index 3febf26..0969f5d 100644
--- a/ytLive.Tests/LayoutStoreSnapshotTests.cs
+++ b/ytLive.Tests/LayoutStoreSnapshotTests.cs
@@ -80,10 +80,57 @@ public sealed class LayoutStoreSnapshotTests
}
}
+ [Fact]
+ public void ResetOneScene_RestoresItsFactoryLayers_AndKeepsOtherSceneEdits()
+ {
+ var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-snapshot-{Guid.NewGuid():N}.db");
+ var starting = MakeScene(SceneCatalog.Starting, "Factory starting");
+ var brb = MakeScene(SceneCatalog.Brb, "Factory BRB");
+ try
+ {
+ using (var store = new LayoutStore(path))
+ {
+ store.Save(new[] { starting, brb }, null, null);
+ store.PrepareLayoutSnapshots("build-a");
+ store.Save(new[]
+ {
+ MakeScene(starting.Id, starting.Name, "Edited starting"),
+ MakeScene(brb.Id, brb.Name, "Edited BRB"),
+ }, null, null);
+ }
+
+ using (var reopened = new LayoutStore(path))
+ {
+ reopened.PrepareLayoutSnapshots("build-a");
+ Assert.True(reopened.RestoreDefaultScene(starting.Id));
+
+ var scenes = reopened.Load();
+ Assert.Equal("Factory starting", Assert.Single(scenes.Single(s => s.Id == starting.Id).Elements.OfType()).Name);
+ Assert.Equal("Edited BRB", Assert.Single(scenes.Single(s => s.Id == brb.Id).Elements.OfType()).Name);
+ }
+ }
+ finally
+ {
+ SqliteConnection.ClearAllPools();
+ try { File.Delete(path); } catch { /* best-effort cleanup */ }
+ }
+ }
+
private static Scene MakeScene(string sourceName)
+ => MakeScene(SceneCatalog.Starting, sourceName);
+
+ private static Scene MakeScene(string sceneName, string sourceName)
=> new()
{
- Name = SceneCatalog.Starting,
+ Name = sceneName,
+ Elements = { new Source { Name = sourceName, Type = SourceType.TextOverlay } },
+ };
+
+ private static Scene MakeScene(string id, string sceneName, string sourceName)
+ => new()
+ {
+ Id = id,
+ Name = sceneName,
Elements = { new Source { Name = sourceName, Type = SourceType.TextOverlay } },
};