feat(37): reset one scene to its factory snapshot
The per-scene reset restores that scene's settings, layers, webcam placement, and background-path settings while leaving other scenes' current edits alone. A focused integration test covers the restore/isolation behavior. 611 tests green, clean build with 0 warnings. Reference: OBS scene collections store scenes and sources as a saved set: https://obsproject.com/kb/scene-collections
This commit is contained in:
@@ -78,6 +78,120 @@ public partial class LayoutStore : IDisposable
|
||||
CaptureDefaults();
|
||||
}
|
||||
|
||||
/// <summary>Restores one existing scene by its current ID. Unknown scenes have no branded default
|
||||
/// and are left untouched.</summary>
|
||||
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<string> 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);
|
||||
|
||||
@@ -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<Source>()).Name);
|
||||
Assert.Equal("Edited BRB", Assert.Single(scenes.Single(s => s.Id == brb.Id).Elements.OfType<Source>()).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 } },
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user