using Microsoft.Data.Sqlite; using ytLive.Models; namespace ytLive.Services; public partial class LayoutStore : IDisposable { public void Save(IEnumerable scenes, Webcam? webcam, SocialsConfig? socials, Music? music = null) { 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 SocialEntry;"; cmd.Transaction = tx; cmd.ExecuteNonQuery(); } using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "DELETE FROM Socials;"; cmd.Transaction = tx; cmd.ExecuteNonQuery(); } using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "DELETE FROM Music;"; 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, HasBackground, HasSocialBar, SortOrder) VALUES ($id, $name, $isHidden, $isChat, $hasBackground, $hasSocialBar, $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 backgroundP = cmd.Parameters.Add("$hasBackground", SqliteType.Integer); var socialBarP = cmd.Parameters.Add("$hasSocialBar", 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; backgroundP.Value = scene.HasBackground ? 1 : 0; socialBarP.Value = scene.HasSocialBar ? 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, IsBackground, CaptureKey, ShowDesktop, SortOrder, ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages, ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor, WebUri, MediaPath, MediaIsLooping, MediaVolume, MediaPlaybackState, AlertVideoAssetId, AlertVideoPath, AlertUseDefaultVideo, AlertShowTicker, AlertVideoVolume, AlertDisplayMethod) VALUES ($id, $sceneId, $assetId, $type, $name, $isEnabled, $x, $y, $w, $h, $opacity, $monitor, $clip, $mirrored, $isBackground, $captureKey, $showDesktop, $sort, $chatFontSize, $chatFontColor, $chatBgColor, $chatMaxMsgs, $chatTimestamps, $chatBadges, $chatMemberColor, $chatSuperChatColor, $webUri, $mediaPath, $mediaIsLooping, $mediaVolume, $mediaPlaybackState, $alertVideoAssetId, $alertVideoPath, $alertUseDefaultVideo, $alertShowTicker, $alertVideoVolume, $alertDisplayMethod) """; 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 isBackgroundP = cmd.Parameters.Add("$isBackground", SqliteType.Integer); var captureKeyP = cmd.Parameters.Add("$captureKey", SqliteType.Text); var showDesktopP = cmd.Parameters.Add("$showDesktop", SqliteType.Integer); var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer); var chatFontSizeP = cmd.Parameters.Add("$chatFontSize", SqliteType.Real); var chatFontColorP = cmd.Parameters.Add("$chatFontColor", SqliteType.Text); var chatBgColorP = cmd.Parameters.Add("$chatBgColor", SqliteType.Text); var chatMaxMsgsP = cmd.Parameters.Add("$chatMaxMsgs", SqliteType.Integer); var chatTimestampsP = cmd.Parameters.Add("$chatTimestamps", SqliteType.Integer); var chatBadgesP = cmd.Parameters.Add("$chatBadges", SqliteType.Integer); var chatMemberColorP = cmd.Parameters.Add("$chatMemberColor", SqliteType.Text); var chatSuperChatColorP = cmd.Parameters.Add("$chatSuperChatColor", SqliteType.Text); var webUriP = cmd.Parameters.Add("$webUri", SqliteType.Text); var mediaPathP = cmd.Parameters.Add("$mediaPath", SqliteType.Text); var mediaIsLoopingP = cmd.Parameters.Add("$mediaIsLooping", SqliteType.Integer); var mediaVolumeP = cmd.Parameters.Add("$mediaVolume", SqliteType.Real); var mediaPlaybackStateP = cmd.Parameters.Add("$mediaPlaybackState", SqliteType.Text); var alertVideoAssetIdP = cmd.Parameters.Add("$alertVideoAssetId", SqliteType.Text); var alertVideoPathP = cmd.Parameters.Add("$alertVideoPath", SqliteType.Text); var alertUseDefaultVideoP = cmd.Parameters.Add("$alertUseDefaultVideo", SqliteType.Integer); var alertShowTickerP = cmd.Parameters.Add("$alertShowTicker", SqliteType.Integer); var alertVideoVolumeP = cmd.Parameters.Add("$alertVideoVolume", SqliteType.Real); var alertDisplayMethodP = cmd.Parameters.Add("$alertDisplayMethod", SqliteType.Integer); // Source and WebcamSceneConfig share ONE z-order space per scene: // each element gets its index within scene.Elements regardless of // table, so a webcam slotted between sources survives save→load. // (Pre-unified rows tie at 0 — load breaks ties sources-first.) foreach (var scene in scenes) { var z = 0; foreach (var element in scene.Elements) { if (element is not Source source) { z++; 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; isBackgroundP.Value = source.IsBackground ? 1 : 0; captureKeyP.Value = (object?)source.CaptureKey ?? DBNull.Value; showDesktopP.Value = source.ShowDesktop ? 1 : 0; sortP.Value = z++; chatFontSizeP.Value = source.ChatFontSize; chatFontColorP.Value = source.ChatFontColor; chatBgColorP.Value = source.ChatBackgroundColor; chatMaxMsgsP.Value = source.ChatMaxMessages; chatTimestampsP.Value = source.ChatShowTimestamps ? 1 : 0; chatBadgesP.Value = source.ChatShowBadges ? 1 : 0; chatMemberColorP.Value = source.ChatMemberColor; chatSuperChatColorP.Value = source.ChatSuperChatColor; webUriP.Value = (object?)source.WebUri ?? DBNull.Value; mediaPathP.Value = (object?)source.MediaPath ?? DBNull.Value; mediaIsLoopingP.Value = source.MediaIsLooping ? 1 : 0; mediaVolumeP.Value = source.MediaVolume; mediaPlaybackStateP.Value = source.MediaPlaybackState.ToString(); alertVideoAssetIdP.Value = (object?)source.AlertVideoAssetId ?? DBNull.Value; alertVideoPathP.Value = (object?)source.AlertVideoPath ?? DBNull.Value; alertUseDefaultVideoP.Value = source.AlertUseDefaultVideo ? 1 : 0; alertShowTickerP.Value = source.AlertShowTicker ? 1 : 0; alertVideoVolumeP.Value = source.AlertVideoVolume; alertDisplayMethodP.Value = (int)source.AlertDisplayMethod; 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 z = 0; foreach (var element in scene.Elements) { if (element is not WebcamSceneConfig config) { z++; continue; } 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 = z++; cmd.ExecuteNonQuery(); } } } } if (socials != null && socials.Entries.Count > 0) { var socialsId = Guid.NewGuid().ToString(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "INSERT INTO Socials (Id, BarPosition, BarEnabled) VALUES ($id, $pos, $enabled);"; cmd.Transaction = tx; cmd.Parameters.AddWithValue("$id", socialsId); cmd.Parameters.AddWithValue("$pos", socials.BarPosition.ToString()); cmd.Parameters.AddWithValue("$enabled", socials.BarEnabled ? 1 : 0); cmd.ExecuteNonQuery(); } using var entryCmd = _connection.CreateCommand(); entryCmd.CommandText = """ INSERT INTO SocialEntry (Id, SocialsId, Service, Handle, ProfileUrl, Software, SortOrder) VALUES ($id, $socialsId, $service, $handle, $url, $software, $sort) """; entryCmd.Transaction = tx; var eId = entryCmd.Parameters.Add("$id", SqliteType.Text); var eSocialsId = entryCmd.Parameters.Add("$socialsId", SqliteType.Text); var eService = entryCmd.Parameters.Add("$service", SqliteType.Text); var eHandle = entryCmd.Parameters.Add("$handle", SqliteType.Text); var eUrl = entryCmd.Parameters.Add("$url", SqliteType.Text); var eSoftware = entryCmd.Parameters.Add("$software", SqliteType.Text); var eSort = entryCmd.Parameters.Add("$sort", SqliteType.Integer); var entrySort = 0; foreach (var entry in socials.Entries) { eId.Value = Guid.NewGuid().ToString(); eSocialsId.Value = socialsId; eService.Value = entry.Service.ToString(); eHandle.Value = entry.Handle; eUrl.Value = entry.ProfileUrl; eSoftware.Value = (object?)entry.FediverseSoftware ?? DBNull.Value; eSort.Value = entrySort++; entryCmd.ExecuteNonQuery(); } } if (music != null && !string.IsNullOrWhiteSpace(music.TrackPath)) { using var cmd = _connection.CreateCommand(); cmd.CommandText = "INSERT INTO Music (Id, TrackPath, IsEnabled) VALUES ($id, $path, $enabled);"; cmd.Transaction = tx; cmd.Parameters.AddWithValue("$id", Guid.NewGuid().ToString()); cmd.Parameters.AddWithValue("$path", music.TrackPath); cmd.Parameters.AddWithValue("$enabled", music.IsEnabled ? 1 : 0); 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 UNION SELECT AlertVideoAssetId FROM Source WHERE AlertVideoAssetId IS NOT NULL UNION SELECT Value FROM Settings WHERE Key = 'AlertDefaultVideoAssetId' ); """; cmd.Transaction = tx; cmd.ExecuteNonQuery(); } // Save custom background paths and use-default flags per background source foreach (var scene in scenes) { var bg = scene.Elements.OfType().FirstOrDefault(s => s.IsBackground); if (bg == null) continue; var pathKey = $"BackgroundPath_{bg.Id}"; if (!string.IsNullOrWhiteSpace(bg.CustomBackgroundPath)) UpsertSetting(pathKey, bg.CustomBackgroundPath); else { using var del = _connection.CreateCommand(); del.CommandText = "DELETE FROM Settings WHERE Key = $key;"; del.Parameters.AddWithValue("$key", pathKey); del.ExecuteNonQuery(); } var defaultKey = $"BackgroundUseDefault_{bg.Id}"; UpsertSetting(defaultKey, bg.UseDefaultBackground ? "1" : "0"); } // Purge background settings left behind by deleted/duplicated rows // (TASK 25 heal drops extras; their keys must not accumulate). using (var purge = _connection.CreateCommand()) { purge.Transaction = tx; purge.CommandText = @"DELETE FROM Settings WHERE (Key LIKE 'BackgroundUseDefault\_%' ESCAPE '\' OR Key LIKE 'BackgroundPath\_%' ESCAPE '\') AND SUBSTR(Key, INSTR(Key, '_') + 1) NOT IN (SELECT Id FROM Source);"; purge.ExecuteNonQuery(); } tx.Commit(); } }