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)
{
ArgumentException.ThrowIfNullOrWhiteSpace(buildId);
var defaultsInitialized = GetSnapshotState(DefaultsInitializedKey) == "1";
if (!defaultsInitialized && HasStoredScenes())
{
CaptureDefaults();
defaultsInitialized = true;
}
var currentBuildId = GetSnapshotState(CurrentBuildIdKey);
if (defaultsInitialized && !string.Equals(currentBuildId, buildId, StringComparison.Ordinal))
RestoreDefaults(buildId);
else if (!defaultsInitialized && string.IsNullOrWhiteSpace(currentBuildId))
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 void RestoreDefaults(string buildId)
{
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 {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 {table} ({columns}) SELECT {columns} FROM Default{table};";
copy.ExecuteNonQuery();
}
using (var clearSettings = _connection.CreateCommand())
{
clearSettings.Transaction = tx;
clearSettings.CommandText = "DELETE FROM Settings WHERE Key LIKE 'BackgroundPath_%' OR Key LIKE 'BackgroundUseDefault_%';";
clearSettings.ExecuteNonQuery();
}
using (var copySettings = _connection.CreateCommand())
{
copySettings.Transaction = tx;
copySettings.CommandText = "INSERT INTO Settings (Key, Value) SELECT Key, Value FROM DefaultLayoutSetting;";
copySettings.ExecuteNonQuery();
}
SetSnapshotState(CurrentBuildIdKey, buildId, tx);
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, SqliteTransaction? transaction = null)
{
using var cmd = _connection.CreateCommand();
cmd.Transaction = transaction;
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("\"", "\"\"") + "\"";
}