674 lines
28 KiB
C#
674 lines
28 KiB
C#
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; }
|
|
|
|
/// <summary>The app-wide webcam identity loaded with the last Load() (null = never picked).</summary>
|
|
public Webcam? Webcam { get; private set; }
|
|
|
|
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 =
|
|
{
|
|
"""
|
|
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,
|
|
HasBackdrop INTEGER NOT NULL DEFAULT 1,
|
|
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,
|
|
ClipShape TEXT NOT NULL DEFAULT 'Traditional',
|
|
IsMirrored INTEGER NOT NULL DEFAULT 0,
|
|
IsBackdrop INTEGER NOT NULL DEFAULT 0,
|
|
CaptureKey TEXT,
|
|
SortOrder INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
""",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS Webcam (
|
|
Id TEXT PRIMARY KEY,
|
|
DeviceId TEXT NOT NULL UNIQUE,
|
|
Name TEXT NOT NULL
|
|
);
|
|
""",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS WebcamSceneConfig (
|
|
SceneId TEXT NOT NULL REFERENCES Scene(Id) ON DELETE CASCADE,
|
|
WebcamId TEXT NOT NULL REFERENCES Webcam(Id) ON DELETE CASCADE,
|
|
IsVisible 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,
|
|
ClipShape TEXT NOT NULL DEFAULT 'Traditional',
|
|
IsMirrored INTEGER NOT NULL DEFAULT 0,
|
|
RectWidth REAL,
|
|
RectHeight REAL,
|
|
BorderColor TEXT,
|
|
BorderOpacity REAL NOT NULL DEFAULT 1,
|
|
BorderWidth INTEGER NOT NULL DEFAULT 0,
|
|
BorderAnimation TEXT NOT NULL DEFAULT 'None',
|
|
SortOrder INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (SceneId, WebcamId)
|
|
);
|
|
""",
|
|
};
|
|
foreach (var sql in statements)
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = sql;
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
MigrateSourceTable();
|
|
MigrateWebcamConfigTable();
|
|
MigrateSceneTable();
|
|
if (GetUserVersion() < 3)
|
|
MigrateToV3();
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "PRAGMA user_version = 6;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
|
|
private int GetUserVersion()
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "PRAGMA user_version;";
|
|
return Convert.ToInt32(cmd.ExecuteScalar());
|
|
}
|
|
|
|
// v1 → v2: Source gains ClipShape + IsMirrored (webcam clip/mirror prefs).
|
|
// CREATE TABLE IF NOT EXISTS doesn't touch existing tables, so pre-v2 DBs
|
|
// get the columns here instead.
|
|
private void MigrateSourceTable()
|
|
{
|
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "PRAGMA table_info(Source);";
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
columns.Add(reader.GetString(1));
|
|
}
|
|
|
|
if (!columns.Contains("ClipShape"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ClipShape TEXT NOT NULL DEFAULT 'Traditional';";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (!columns.Contains("IsMirrored"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN IsMirrored INTEGER NOT NULL DEFAULT 0;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (!columns.Contains("IsBackdrop"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN IsBackdrop INTEGER NOT NULL DEFAULT 0;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (!columns.Contains("CaptureKey"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN CaptureKey TEXT;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
|
|
// v3 → v4: WebcamSceneConfig gains RectWidth/RectHeight (the pre-Round rect,
|
|
// so a reloaded Round webcam restores its aspect on toggle-back).
|
|
private void MigrateWebcamConfigTable()
|
|
{
|
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "PRAGMA table_info(WebcamSceneConfig);";
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
columns.Add(reader.GetString(1));
|
|
}
|
|
|
|
if (!columns.Contains("RectWidth"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE WebcamSceneConfig ADD COLUMN RectWidth REAL;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
if (!columns.Contains("RectHeight"))
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = "ALTER TABLE WebcamSceneConfig ADD COLUMN RectHeight REAL;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
|
|
// v2 → v3: the webcam leaves the Source table. Any webcam Source rows become
|
|
// one Webcam identity (first row — one camera app-wide) + a WebcamSceneConfig
|
|
// per scene that had one, then the webcam rows are deleted. No backfill:
|
|
// scenes without the webcam stay webcam-free.
|
|
private void MigrateToV3()
|
|
{
|
|
var rows = new List<(string Id, string SceneId, string Name, double X, double Y, double W, double H, double Opacity, string DeviceId, string ClipShape, bool IsMirrored)>();
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = """
|
|
SELECT Id, SceneId, Name, X, Y, Width, Height, Opacity, DeviceId, ClipShape, IsMirrored
|
|
FROM Source WHERE Type = 'Webcam' ORDER BY SortOrder;
|
|
""";
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
{
|
|
rows.Add((
|
|
reader.GetString(0),
|
|
reader.GetString(1),
|
|
reader.GetString(2),
|
|
reader.GetDouble(3),
|
|
reader.GetDouble(4),
|
|
reader.GetDouble(5),
|
|
reader.GetDouble(6),
|
|
reader.GetDouble(7),
|
|
reader.IsDBNull(8) ? string.Empty : reader.GetString(8),
|
|
reader.GetString(9),
|
|
reader.GetInt32(10) != 0));
|
|
}
|
|
}
|
|
if (rows.Count == 0) return;
|
|
|
|
using var tx = _connection.BeginTransaction();
|
|
var webcamId = Guid.NewGuid().ToString();
|
|
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "INSERT INTO Webcam (Id, DeviceId, Name) VALUES ($id, $device, $name);";
|
|
cmd.Transaction = tx;
|
|
cmd.Parameters.AddWithValue("$id", webcamId);
|
|
cmd.Parameters.AddWithValue("$device", rows[0].DeviceId);
|
|
cmd.Parameters.AddWithValue("$name", rows[0].Name);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
foreach (var row in rows)
|
|
{
|
|
using var cmd = _connection.CreateCommand();
|
|
cmd.CommandText = """
|
|
INSERT INTO WebcamSceneConfig
|
|
(SceneId, WebcamId, IsVisible, X, Y, Width, Height, Opacity,
|
|
ClipShape, IsMirrored, BorderColor, BorderOpacity, BorderWidth, BorderAnimation, SortOrder)
|
|
VALUES ($sceneId, $webcamId, 1, $x, $y, $w, $h, $opacity,
|
|
$clip, $mirrored, NULL, 1, 0, 'None', 0);
|
|
""";
|
|
cmd.Transaction = tx;
|
|
cmd.Parameters.AddWithValue("$sceneId", row.SceneId);
|
|
cmd.Parameters.AddWithValue("$webcamId", webcamId);
|
|
cmd.Parameters.AddWithValue("$x", row.X);
|
|
cmd.Parameters.AddWithValue("$y", row.Y);
|
|
cmd.Parameters.AddWithValue("$w", row.W);
|
|
cmd.Parameters.AddWithValue("$h", row.H);
|
|
cmd.Parameters.AddWithValue("$opacity", row.Opacity);
|
|
cmd.Parameters.AddWithValue("$clip", row.ClipShape);
|
|
cmd.Parameters.AddWithValue("$mirrored", row.IsMirrored ? 1 : 0);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "DELETE FROM Source WHERE Type = 'Webcam';";
|
|
cmd.Transaction = tx;
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
tx.Commit();
|
|
}
|
|
|
|
// v5 → v6: Scene gains HasBackdrop. Added columns default to true; the
|
|
// ONE-TIME backfill (runs only when the column is first added) turns every
|
|
// non-Live canonical scene off and drops their backdrop sources — the fix for
|
|
// DBs saved before the Live-only policy. MainViewModel.EnforceBackdropPolicy
|
|
// re-runs the same normalization on every load for DBs that miss the backfill.
|
|
private void MigrateSceneTable()
|
|
{
|
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "PRAGMA table_info(Scene);";
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
columns.Add(reader.GetString(1));
|
|
}
|
|
|
|
if (columns.Contains("HasBackdrop")) return;
|
|
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "ALTER TABLE Scene ADD COLUMN HasBackdrop INTEGER NOT NULL DEFAULT 1;";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
const string noBackdrop = "('starting', 'brb', 'chat', 'ending')";
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = $"UPDATE Scene SET HasBackdrop = 0 WHERE LOWER(TRIM(Name)) IN {noBackdrop};";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = $"""
|
|
DELETE FROM Source
|
|
WHERE IsBackdrop = 1 AND SceneId IN
|
|
(SELECT Id FROM Scene WHERE LOWER(TRIM(Name)) IN {noBackdrop});
|
|
""";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
|
|
public List<Scene> Load()
|
|
{
|
|
Webcam = null;
|
|
var scenes = new List<Scene>();
|
|
var sourcesByScene = new Dictionary<string, List<Source>>();
|
|
var configsByScene = new Dictionary<string, List<WebcamSceneConfig>>();
|
|
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "SELECT Id, Name, IsHidden, IsChatScene, HasBackdrop 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,
|
|
HasBackdrop = reader.GetInt32(4) != 0,
|
|
});
|
|
}
|
|
}
|
|
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "SELECT Id, DeviceId, Name FROM Webcam;";
|
|
using var reader = cmd.ExecuteReader();
|
|
if (reader.Read())
|
|
{
|
|
Webcam = new Webcam
|
|
{
|
|
Id = reader.GetString(0),
|
|
DeviceId = reader.GetString(1),
|
|
Name = reader.GetString(2),
|
|
};
|
|
}
|
|
}
|
|
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = """
|
|
SELECT Id, SceneId, AssetId, Type, Name, IsEnabled,
|
|
X, Y, Width, Height, Opacity, MonitorIndex, ClipShape, IsMirrored,
|
|
IsBackdrop, CaptureKey
|
|
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),
|
|
ClipShape = Enum.TryParse<ClipShape>(reader.GetString(12), out var clip) ? clip : ClipShape.Traditional,
|
|
IsMirrored = reader.GetInt32(13) != 0,
|
|
IsBackdrop = reader.GetInt32(14) != 0,
|
|
CaptureKey = reader.IsDBNull(15) ? null : reader.GetString(15),
|
|
};
|
|
if (!sourcesByScene.TryGetValue(sceneId, out var list))
|
|
sourcesByScene[sceneId] = list = new List<Source>();
|
|
list.Add(source);
|
|
}
|
|
}
|
|
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = """
|
|
SELECT SceneId, WebcamId, IsVisible, X, Y, Width, Height, Opacity,
|
|
ClipShape, IsMirrored, BorderColor, BorderOpacity, BorderWidth, BorderAnimation,
|
|
RectWidth, RectHeight
|
|
FROM WebcamSceneConfig ORDER BY SortOrder
|
|
""";
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
{
|
|
var sceneId = reader.GetString(0);
|
|
var config = new WebcamSceneConfig
|
|
{
|
|
WebcamId = reader.GetString(1),
|
|
Name = Webcam?.Name ?? "Webcam",
|
|
IsVisible = reader.GetInt32(2) != 0,
|
|
X = reader.GetDouble(3),
|
|
Y = reader.GetDouble(4),
|
|
Width = reader.GetDouble(5),
|
|
Height = reader.GetDouble(6),
|
|
Opacity = reader.GetDouble(7),
|
|
ClipShape = Enum.TryParse<ClipShape>(reader.GetString(8), out var clip) ? clip : ClipShape.Traditional,
|
|
IsMirrored = reader.GetInt32(9) != 0,
|
|
BorderColor = reader.IsDBNull(10) ? string.Empty : reader.GetString(10),
|
|
BorderOpacity = reader.IsDBNull(11) ? 1.0 : reader.GetDouble(11),
|
|
BorderWidth = reader.IsDBNull(12) ? 0 : reader.GetInt32(12),
|
|
BorderAnimation = Enum.TryParse<BorderAnimation>(reader.GetString(13), out var anim) ? anim : BorderAnimation.None,
|
|
RectWidth = reader.IsDBNull(14) ? null : reader.GetDouble(14),
|
|
RectHeight = reader.IsDBNull(15) ? null : reader.GetDouble(15),
|
|
};
|
|
if (!configsByScene.TryGetValue(sceneId, out var list))
|
|
configsByScene[sceneId] = list = new List<WebcamSceneConfig>();
|
|
list.Add(config);
|
|
}
|
|
}
|
|
|
|
foreach (var scene in scenes)
|
|
{
|
|
if (sourcesByScene.TryGetValue(scene.Id, out var sources))
|
|
foreach (var source in sources)
|
|
scene.Elements.Add(source);
|
|
if (configsByScene.TryGetValue(scene.Id, out var configs))
|
|
foreach (var config in configs)
|
|
scene.Elements.Add(config);
|
|
}
|
|
|
|
return scenes;
|
|
}
|
|
|
|
public void Save(IEnumerable<Scene> scenes, Webcam? webcam)
|
|
{
|
|
using var tx = _connection.BeginTransaction();
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "DELETE FROM WebcamSceneConfig;";
|
|
cmd.Transaction = tx;
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "DELETE FROM Source;";
|
|
cmd.Transaction = tx;
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "DELETE FROM Webcam;";
|
|
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, HasBackdrop, SortOrder)
|
|
VALUES ($id, $name, $isHidden, $isChat, $hasBackdrop, $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 backdropP = cmd.Parameters.Add("$hasBackdrop", 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;
|
|
backdropP.Value = scene.HasBackdrop ? 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,
|
|
ClipShape, IsMirrored, IsBackdrop, CaptureKey, SortOrder)
|
|
VALUES ($id, $sceneId, $assetId, $type, $name, $isEnabled,
|
|
$x, $y, $w, $h, $opacity, $monitor,
|
|
$clip, $mirrored, $isBackdrop, $captureKey, $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 clipP = cmd.Parameters.Add("$clip", SqliteType.Text);
|
|
var mirroredP = cmd.Parameters.Add("$mirrored", SqliteType.Integer);
|
|
var isBackdropP = cmd.Parameters.Add("$isBackdrop", SqliteType.Integer);
|
|
var captureKeyP = cmd.Parameters.Add("$captureKey", SqliteType.Text);
|
|
var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer);
|
|
|
|
foreach (var scene in scenes)
|
|
{
|
|
var sort = 0;
|
|
foreach (var element in scene.Elements)
|
|
{
|
|
if (element is not Source source) continue;
|
|
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;
|
|
clipP.Value = source.ClipShape.ToString();
|
|
mirroredP.Value = source.IsMirrored ? 1 : 0;
|
|
isBackdropP.Value = source.IsBackdrop ? 1 : 0;
|
|
captureKeyP.Value = (object?)source.CaptureKey ?? DBNull.Value;
|
|
sortP.Value = sort++;
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (webcam != null)
|
|
{
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = "INSERT INTO Webcam (Id, DeviceId, Name) VALUES ($id, $device, $name);";
|
|
cmd.Transaction = tx;
|
|
cmd.Parameters.AddWithValue("$id", webcam.Id);
|
|
cmd.Parameters.AddWithValue("$device", webcam.DeviceId);
|
|
cmd.Parameters.AddWithValue("$name", webcam.Name);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
using (var cmd = _connection.CreateCommand())
|
|
{
|
|
cmd.CommandText = """
|
|
INSERT INTO WebcamSceneConfig
|
|
(SceneId, WebcamId, IsVisible, X, Y, Width, Height, Opacity,
|
|
ClipShape, IsMirrored, BorderColor, BorderOpacity, BorderWidth, BorderAnimation,
|
|
RectWidth, RectHeight, SortOrder)
|
|
VALUES ($sceneId, $webcamId, $isVisible, $x, $y, $w, $h, $opacity,
|
|
$clip, $mirrored, $borderColor, $borderOpacity, $borderWidth, $borderAnimation,
|
|
$rectWidth, $rectHeight, $sort)
|
|
""";
|
|
cmd.Transaction = tx;
|
|
var sceneIdP = cmd.Parameters.Add("$sceneId", SqliteType.Text);
|
|
var webcamIdP = cmd.Parameters.Add("$webcamId", SqliteType.Text);
|
|
var visibleP = cmd.Parameters.Add("$isVisible", 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 clipP = cmd.Parameters.Add("$clip", SqliteType.Text);
|
|
var mirroredP = cmd.Parameters.Add("$mirrored", SqliteType.Integer);
|
|
var colorP = cmd.Parameters.Add("$borderColor", SqliteType.Text);
|
|
var borderOpacityP = cmd.Parameters.Add("$borderOpacity", SqliteType.Real);
|
|
var borderWidthP = cmd.Parameters.Add("$borderWidth", SqliteType.Integer);
|
|
var animationP = cmd.Parameters.Add("$borderAnimation", SqliteType.Text);
|
|
var rectWidthP = cmd.Parameters.Add("$rectWidth", SqliteType.Real);
|
|
var rectHeightP = cmd.Parameters.Add("$rectHeight", SqliteType.Real);
|
|
var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer);
|
|
|
|
foreach (var scene in scenes)
|
|
{
|
|
var sort = 0;
|
|
foreach (var config in scene.Elements.OfType<WebcamSceneConfig>())
|
|
{
|
|
sceneIdP.Value = scene.Id;
|
|
webcamIdP.Value = config.WebcamId;
|
|
visibleP.Value = config.IsVisible ? 1 : 0;
|
|
xP.Value = config.X;
|
|
yP.Value = config.Y;
|
|
wP.Value = config.Width;
|
|
hP.Value = config.Height;
|
|
opacityP.Value = config.Opacity;
|
|
clipP.Value = config.ClipShape.ToString();
|
|
mirroredP.Value = config.IsMirrored ? 1 : 0;
|
|
colorP.Value = (object?)(string.IsNullOrEmpty(config.BorderColor) ? null : config.BorderColor) ?? DBNull.Value;
|
|
borderOpacityP.Value = config.BorderOpacity;
|
|
borderWidthP.Value = config.BorderWidth;
|
|
animationP.Value = config.BorderAnimation.ToString();
|
|
rectWidthP.Value = (object?)config.RectWidth ?? DBNull.Value;
|
|
rectHeightP.Value = (object?)config.RectHeight ?? 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();
|
|
}
|
|
}
|