using Microsoft.Data.Sqlite; namespace ytLive.Services; public partial class LayoutStore : IDisposable { private static readonly string[] SnapshotTables = { "Scene", "Source", "Webcam", "WebcamSceneConfig", "Socials", "SocialEntry", "Music", }; private const string DefaultsInitializedKey = "DefaultsInitialized"; private const string CurrentBuildIdKey = "CurrentBuildId"; private void EnsureLayoutSnapshotSchema() { using (var cmd = _connection.CreateCommand()) { cmd.CommandText = """ CREATE TABLE IF NOT EXISTS LayoutSnapshotState ( Key TEXT PRIMARY KEY, Value TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS DefaultLayoutSetting ( Key TEXT PRIMARY KEY, Value TEXT NOT NULL ); """; cmd.ExecuteNonQuery(); } foreach (var table in SnapshotTables) { var snapshotTable = "Default" + table; using (var create = _connection.CreateCommand()) { create.CommandText = $"CREATE TABLE IF NOT EXISTS {snapshotTable} AS SELECT * FROM {table} WHERE 0;"; create.ExecuteNonQuery(); } var snapshotColumns = ReadColumnNames(snapshotTable); foreach (var (name, type, defaultValue) in ReadColumnDefinitions(table)) { if (snapshotColumns.Contains(name, StringComparer.OrdinalIgnoreCase)) continue; var definition = string.IsNullOrWhiteSpace(type) ? "BLOB" : type; if (defaultValue != null) definition += " DEFAULT " + defaultValue; using var add = _connection.CreateCommand(); add.CommandText = $"ALTER TABLE {snapshotTable} ADD COLUMN {QuoteIdentifier(name)} {definition};"; add.ExecuteNonQuery(); } } } /// Captures the installed layout once as the immutable branded default. Existing databases /// are snapshotted before MainViewModel loads them; a fresh database calls /// after its factory scenes have been seeded. public void PrepareLayoutSnapshots(string buildId) { if (GetSnapshotState(DefaultsInitializedKey) != "1" && HasStoredScenes()) CaptureDefaults(); if (string.IsNullOrWhiteSpace(GetSnapshotState(CurrentBuildIdKey))) SetSnapshotState(CurrentBuildIdKey, buildId); } /// Seeds the default snapshot after the first-run factory layout has been persisted. public void CaptureDefaultsIfMissing() { if (GetSnapshotState(DefaultsInitializedKey) != "1") CaptureDefaults(); } private void CaptureDefaults() { var columnsByTable = SnapshotTables.ToDictionary(table => table, ReadColumnNames); using var tx = _connection.BeginTransaction(); foreach (var table in SnapshotTables.Reverse()) { using var delete = _connection.CreateCommand(); delete.Transaction = tx; delete.CommandText = $"DELETE FROM Default{table};"; delete.ExecuteNonQuery(); } foreach (var table in SnapshotTables) { var columns = string.Join(", ", columnsByTable[table].Select(QuoteIdentifier)); using var copy = _connection.CreateCommand(); copy.Transaction = tx; copy.CommandText = $"INSERT INTO Default{table} ({columns}) SELECT {columns} FROM {table};"; copy.ExecuteNonQuery(); } using (var clearSettings = _connection.CreateCommand()) { clearSettings.Transaction = tx; clearSettings.CommandText = "DELETE FROM DefaultLayoutSetting;"; clearSettings.ExecuteNonQuery(); } using (var copySettings = _connection.CreateCommand()) { copySettings.Transaction = tx; copySettings.CommandText = """ INSERT INTO DefaultLayoutSetting (Key, Value) SELECT Key, Value FROM Settings WHERE Key LIKE 'BackgroundPath_%' OR Key LIKE 'BackgroundUseDefault_%'; """; copySettings.ExecuteNonQuery(); } using (var state = _connection.CreateCommand()) { state.Transaction = tx; state.CommandText = """ INSERT INTO LayoutSnapshotState (Key, Value) VALUES ($key, '1') ON CONFLICT(Key) DO UPDATE SET Value = excluded.Value; """; state.Parameters.AddWithValue("$key", DefaultsInitializedKey); state.ExecuteNonQuery(); } tx.Commit(); } private bool HasStoredScenes() { using var cmd = _connection.CreateCommand(); cmd.CommandText = "SELECT EXISTS(SELECT 1 FROM Scene LIMIT 1);"; return Convert.ToInt32(cmd.ExecuteScalar()) != 0; } private string? GetSnapshotState(string key) { using var cmd = _connection.CreateCommand(); cmd.CommandText = "SELECT Value FROM LayoutSnapshotState WHERE Key = $key;"; cmd.Parameters.AddWithValue("$key", key); return cmd.ExecuteScalar() as string; } private void SetSnapshotState(string key, string value) { using var cmd = _connection.CreateCommand(); cmd.CommandText = """ INSERT INTO LayoutSnapshotState (Key, Value) VALUES ($key, $value) ON CONFLICT(Key) DO UPDATE SET Value = excluded.Value; """; cmd.Parameters.AddWithValue("$key", key); cmd.Parameters.AddWithValue("$value", value); cmd.ExecuteNonQuery(); } private List ReadColumnNames(string table) => ReadColumnDefinitions(table).Select(column => column.Name).ToList(); private List<(string Name, string Type, string? DefaultValue)> ReadColumnDefinitions(string table) { var columns = new List<(string, string, string?)>(); using var cmd = _connection.CreateCommand(); cmd.CommandText = $"PRAGMA table_info({table});"; using var reader = cmd.ExecuteReader(); while (reader.Read()) columns.Add((reader.GetString(1), reader.IsDBNull(2) ? string.Empty : reader.GetString(2), reader.IsDBNull(4) ? null : reader.GetString(4))); return columns; } private static string QuoteIdentifier(string identifier) => "\"" + identifier.Replace("\"", "\"\"") + "\""; }