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 } }, };