using System.IO; using System.Security.Cryptography; using Microsoft.Data.Sqlite; using ytLive.Models; namespace ytLive.Services; public class LayoutStore : IDisposable { public static LayoutStore? Instance { get; private set; } private readonly SqliteConnection _connection; public string ActivePath { get; } public LayoutStore(string path) { ActivePath = path; Directory.CreateDirectory(Path.GetDirectoryName(path)!); _connection = new SqliteConnection($"Data Source={path}"); _connection.Open(); using (var pragma = _connection.CreateCommand()) { pragma.CommandText = "PRAGMA foreign_keys = ON;"; pragma.ExecuteNonQuery(); } EnsureSchema(); Instance = this; } private void EnsureSchema() { string[] statements = { "PRAGMA user_version = 1;", """ CREATE TABLE IF NOT EXISTS Scene ( Id TEXT PRIMARY KEY, Name TEXT NOT NULL, IsHidden INTEGER NOT NULL DEFAULT 0, IsChatScene INTEGER NOT NULL DEFAULT 0, SortOrder INTEGER NOT NULL DEFAULT 0 ); """, """ CREATE TABLE IF NOT EXISTS Asset ( Id TEXT PRIMARY KEY, Hash TEXT NOT NULL UNIQUE, Data BLOB NOT NULL, PixelWidth INTEGER NOT NULL DEFAULT 0, PixelHeight INTEGER NOT NULL DEFAULT 0 ); """, """ CREATE TABLE IF NOT EXISTS Source ( Id TEXT PRIMARY KEY, SceneId TEXT NOT NULL REFERENCES Scene(Id) ON DELETE CASCADE, AssetId TEXT REFERENCES Asset(Id), Type TEXT NOT NULL, Name TEXT NOT NULL, IsEnabled INTEGER NOT NULL DEFAULT 1, X REAL NOT NULL DEFAULT 0, Y REAL NOT NULL DEFAULT 0, Width REAL NOT NULL DEFAULT 0, Height REAL NOT NULL DEFAULT 0, Opacity REAL NOT NULL DEFAULT 1, MonitorIndex INTEGER, DeviceId TEXT, SortOrder INTEGER NOT NULL DEFAULT 0 ); """, }; foreach (var sql in statements) { using var cmd = _connection.CreateCommand(); cmd.CommandText = sql; cmd.ExecuteNonQuery(); } } public List Load() { var scenes = new List(); var sourcesByScene = new Dictionary>(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "SELECT Id, Name, IsHidden, IsChatScene FROM Scene ORDER BY SortOrder"; using var reader = cmd.ExecuteReader(); while (reader.Read()) { scenes.Add(new Scene { Id = reader.GetString(0), Name = reader.GetString(1), IsHidden = reader.GetInt32(2) != 0, IsChatScene = reader.GetInt32(3) != 0, }); } } using (var cmd = _connection.CreateCommand()) { cmd.CommandText = """ SELECT Id, SceneId, AssetId, Type, Name, IsEnabled, X, Y, Width, Height, Opacity, MonitorIndex, DeviceId FROM Source ORDER BY SortOrder """; using var reader = cmd.ExecuteReader(); while (reader.Read()) { var sceneId = reader.GetString(1); var source = new Source { Id = reader.GetString(0), AssetId = reader.IsDBNull(2) ? null : reader.GetString(2), Type = Enum.Parse(reader.GetString(3)), Name = reader.GetString(4), IsEnabled = reader.GetInt32(5) != 0, X = reader.GetDouble(6), Y = reader.GetDouble(7), Width = reader.GetDouble(8), Height = reader.GetDouble(9), Opacity = reader.GetDouble(10), MonitorIndex = reader.IsDBNull(11) ? null : reader.GetInt32(11), DeviceId = reader.IsDBNull(12) ? null : reader.GetString(12), }; if (!sourcesByScene.TryGetValue(sceneId, out var list)) sourcesByScene[sceneId] = list = new List(); list.Add(source); } } foreach (var scene in scenes) { if (sourcesByScene.TryGetValue(scene.Id, out var list)) foreach (var source in list) scene.Sources.Add(source); } return scenes; } public void Save(IEnumerable scenes) { using var tx = _connection.BeginTransaction(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "DELETE FROM Source;"; cmd.Transaction = tx; cmd.ExecuteNonQuery(); } using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "DELETE FROM Scene;"; cmd.Transaction = tx; cmd.ExecuteNonQuery(); } using (var cmd = _connection.CreateCommand()) { cmd.CommandText = """ INSERT INTO Scene (Id, Name, IsHidden, IsChatScene, SortOrder) VALUES ($id, $name, $isHidden, $isChat, $sort) """; cmd.Transaction = tx; var idP = cmd.Parameters.Add("$id", SqliteType.Text); var nameP = cmd.Parameters.Add("$name", SqliteType.Text); var hiddenP = cmd.Parameters.Add("$isHidden", SqliteType.Integer); var chatP = cmd.Parameters.Add("$isChat", SqliteType.Integer); var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer); var sort = 0; foreach (var scene in scenes) { idP.Value = scene.Id; nameP.Value = scene.Name; hiddenP.Value = scene.IsHidden ? 1 : 0; chatP.Value = scene.IsChatScene ? 1 : 0; sortP.Value = sort++; cmd.ExecuteNonQuery(); } } using (var cmd = _connection.CreateCommand()) { cmd.CommandText = """ INSERT INTO Source (Id, SceneId, AssetId, Type, Name, IsEnabled, X, Y, Width, Height, Opacity, MonitorIndex, DeviceId, SortOrder) VALUES ($id, $sceneId, $assetId, $type, $name, $isEnabled, $x, $y, $w, $h, $opacity, $monitor, $device, $sort) """; cmd.Transaction = tx; var idP = cmd.Parameters.Add("$id", SqliteType.Text); var sceneIdP = cmd.Parameters.Add("$sceneId", SqliteType.Text); var assetIdP = cmd.Parameters.Add("$assetId", SqliteType.Text); var typeP = cmd.Parameters.Add("$type", SqliteType.Text); var nameP = cmd.Parameters.Add("$name", SqliteType.Text); var enabledP = cmd.Parameters.Add("$isEnabled", SqliteType.Integer); var xP = cmd.Parameters.Add("$x", SqliteType.Real); var yP = cmd.Parameters.Add("$y", SqliteType.Real); var wP = cmd.Parameters.Add("$w", SqliteType.Real); var hP = cmd.Parameters.Add("$h", SqliteType.Real); var opacityP = cmd.Parameters.Add("$opacity", SqliteType.Real); var monitorP = cmd.Parameters.Add("$monitor", SqliteType.Integer); var deviceP = cmd.Parameters.Add("$device", SqliteType.Text); var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer); foreach (var scene in scenes) { var sort = 0; foreach (var source in scene.Sources) { idP.Value = source.Id; sceneIdP.Value = scene.Id; assetIdP.Value = (object?)source.AssetId ?? DBNull.Value; typeP.Value = source.Type.ToString(); nameP.Value = source.Name; enabledP.Value = source.IsEnabled ? 1 : 0; xP.Value = source.X; yP.Value = source.Y; wP.Value = source.Width; hP.Value = source.Height; opacityP.Value = source.Opacity; monitorP.Value = (object?)source.MonitorIndex ?? DBNull.Value; deviceP.Value = (object?)source.DeviceId ?? DBNull.Value; sortP.Value = sort++; cmd.ExecuteNonQuery(); } } } using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "DELETE FROM Asset WHERE Id NOT IN (SELECT AssetId FROM Source WHERE AssetId IS NOT NULL);"; cmd.Transaction = tx; cmd.ExecuteNonQuery(); } tx.Commit(); } public byte[]? GetAssetBytes(string assetId) { using var cmd = _connection.CreateCommand(); cmd.CommandText = "SELECT Data FROM Asset WHERE Id = $id"; cmd.Parameters.AddWithValue("$id", assetId); return cmd.ExecuteScalar() as byte[]; } public string UpsertAsset(byte[] data, int pixelWidth, int pixelHeight) { var hash = Convert.ToHexString(SHA256.HashData(data)); var id = Guid.NewGuid().ToString(); using var cmd = _connection.CreateCommand(); cmd.CommandText = """ INSERT INTO Asset (Id, Hash, Data, PixelWidth, PixelHeight) VALUES ($id, $hash, $data, $w, $h) ON CONFLICT(Hash) DO NOTHING; """; cmd.Parameters.AddWithValue("$id", id); cmd.Parameters.AddWithValue("$hash", hash); cmd.Parameters.AddWithValue("$data", data); cmd.Parameters.AddWithValue("$w", pixelWidth); cmd.Parameters.AddWithValue("$h", pixelHeight); cmd.ExecuteNonQuery(); cmd.CommandText = "SELECT Id FROM Asset WHERE Hash = $hash;"; return (string)cmd.ExecuteScalar()!; } public void Dispose() { _connection.Dispose(); } }