Scene/source builder with image overlays, reuse dialog, and SQLite layout persistence
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
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<Scene> Load()
|
||||
{
|
||||
var scenes = new List<Scene>();
|
||||
var sourcesByScene = new Dictionary<string, List<Source>>();
|
||||
|
||||
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<SourceType>(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<Source>();
|
||||
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<Scene> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user