Files
LlamaCasty/Services/LayoutStore.Save.cs
T
gramps c257de1516 feat(35): scenes remember the room — BRB mutes the mic on entry
591 green, 0 warnings.

- `Scene.SceneMutesMic` / `SceneMutesDesktop` are **nullable booleans**: null = leave the mix alone, so
  a scene is passive until the creator says otherwise. Schema columns added, NULL for every existing
  layout except one deliberate default.
- **Applied once when the scene takes the PROGRAM output** (`TransitionToScene`, only when live or
  recording). Staging a scene offline is auditioning, not a stream edit — the test asserts that.
- **The creator is boss:** a manual mute survives any transition into a scene with no opinion; a scene
  that does have one wins on its next entry. `SetMicMuted`/`SetGameMuted` are idempotent so a repeated
  "mute" cannot clobber the level that unmute would restore.
- **BRB ships muted** (creator spec) for new layouts *and* via a one-time migration for existing rows
  where the creator has never expressed an opinion.
- UI: two three-state checkboxes under the layers list ("When this scene goes live"), styled with
  `PropertyPillCheck` so the theme test holds.

`SceneAudioTests` covers the whole rule in one integration test: default state, offline staging does
nothing, live entry mutes, manual unmute survives a passive scene, an opinionated scene wins, desktop
untouched, and all three states roundtrip through save/reload.

Needs the creator's eye: the checkboxes are new chrome in the left panel (indeterminate = passive).
2026-11-10 11:51:21 -08:00

485 lines
28 KiB
C#

using Microsoft.Data.Sqlite;
using ytLive.Models;
namespace ytLive.Services;
public partial class LayoutStore : IDisposable
{
public void Save(IEnumerable<Scene> 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, ThumbnailAssetId,
SceneMutesMic, SceneMutesDesktop, SortOrder)
VALUES ($id, $name, $isHidden, $isChat, $hasBackground, $hasSocialBar, $thumbnail,
$mutesMic, $mutesDesktop, $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 thumbnailP = cmd.Parameters.Add("$thumbnail", SqliteType.Text);
var mutesMicP = cmd.Parameters.Add("$mutesMic", SqliteType.Integer);
var mutesDesktopP = cmd.Parameters.Add("$mutesDesktop", 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;
thumbnailP.Value = (object?)scene.ThumbnailAssetId ?? DBNull.Value;
mutesMicP.Value = scene.SceneMutesMic.HasValue
? (object)(scene.SceneMutesMic.Value ? 1 : 0)
: DBNull.Value;
mutesDesktopP.Value = scene.SceneMutesDesktop.HasValue
? (object)(scene.SceneMutesDesktop.Value ? 1 : 0)
: DBNull.Value;
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, IsBrandLogo, BlurAmount, AlertAudioPath, AlertAudioLoop,
TextContent, TextFontFamily, TextFontSize, TextColor, TextBold, TextItalic,
TextUnderline, TextAlignment, TextVerticalAlignment, MediaMuted,
ChromaKeyEnabled, ChromaKeyColor, ChromaKeySimilarity,
ChromaKeySoftness, ChromaKeySpill, ChromaScreen)
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, $isBrandLogo, $blurAmount, $alertAudioPath, $alertAudioLoop,
$textContent, $textFontFamily, $textFontSize, $textColor, $textBold, $textItalic,
$textUnderline, $textAlignment, $textVerticalAlignment, $mediaMuted,
$chromaEnabled, $chromaColor, $chromaSimilarity, $chromaSoftness,
$chromaSpill, $chromaScreen)
""";
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 mediaMutedP = cmd.Parameters.Add("$mediaMuted", SqliteType.Integer);
var chromaEnabledP = cmd.Parameters.Add("$chromaEnabled", SqliteType.Integer);
var chromaColorP = cmd.Parameters.Add("$chromaColor", SqliteType.Text);
var chromaSimilarityP = cmd.Parameters.Add("$chromaSimilarity", SqliteType.Real);
var chromaSoftnessP = cmd.Parameters.Add("$chromaSoftness", SqliteType.Real);
var chromaSpillP = cmd.Parameters.Add("$chromaSpill", SqliteType.Integer);
var chromaScreenP = cmd.Parameters.Add("$chromaScreen", SqliteType.Integer);
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);
var isBrandLogoP = cmd.Parameters.Add("$isBrandLogo", SqliteType.Integer);
cmd.Parameters.Add("$blurAmount", SqliteType.Real);
var alertAudioPathP = cmd.Parameters.Add("$alertAudioPath", SqliteType.Text);
var alertAudioLoopP = cmd.Parameters.Add("$alertAudioLoop", SqliteType.Integer);
var textContentP = cmd.Parameters.Add("$textContent", SqliteType.Text);
var textFontFamilyP = cmd.Parameters.Add("$textFontFamily", SqliteType.Text);
var textFontSizeP = cmd.Parameters.Add("$textFontSize", SqliteType.Real);
var textColorP = cmd.Parameters.Add("$textColor", SqliteType.Text);
var textBoldP = cmd.Parameters.Add("$textBold", SqliteType.Integer);
var textItalicP = cmd.Parameters.Add("$textItalic", SqliteType.Integer);
var textUnderlineP = cmd.Parameters.Add("$textUnderline", SqliteType.Integer);
var textAlignmentP = cmd.Parameters.Add("$textAlignment", SqliteType.Integer);
var textVerticalAlignmentP = cmd.Parameters.Add("$textVerticalAlignment", 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;
mediaMutedP.Value = source.MediaMuted ? 1 : 0;
chromaEnabledP.Value = source.ChromaKeyEnabled ? 1 : 0;
chromaColorP.Value = source.ChromaKeyColor;
chromaSimilarityP.Value = source.ChromaKeySimilarity;
chromaSoftnessP.Value = source.ChromaKeySoftness;
chromaSpillP.Value = source.ChromaKeySpill ? 1 : 0;
chromaScreenP.Value = (int)source.ChromaScreen;
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;
isBrandLogoP.Value = source.IsBrandLogo ? 1 : 0;
cmd.Parameters["$blurAmount"].Value = source.BlurAmount;
alertAudioPathP.Value = (object?)source.AlertAudioPath ?? DBNull.Value;
alertAudioLoopP.Value = source.AlertAudioLoop ? 1 : 0;
textContentP.Value = source.TextContent;
textFontFamilyP.Value = source.TextFontFamily;
textFontSizeP.Value = source.TextFontSize;
textColorP.Value = source.TextColor;
textBoldP.Value = source.TextBold ? 1 : 0;
textItalicP.Value = source.TextItalic ? 1 : 0;
textUnderlineP.Value = source.TextUnderline ? 1 : 0;
textAlignmentP.Value = (int)source.TextAlignment;
textVerticalAlignmentP.Value = (int)source.TextVerticalAlignment;
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, BackgroundEffect, BackgroundStrength,
VerticalX, VerticalY, VerticalWidth, VerticalHeight, VerticalClipShape,
VerticalRectWidth, VerticalRectHeight)
VALUES ($sceneId, $webcamId, $isVisible, $x, $y, $w, $h, $opacity,
$clip, $mirrored, $borderColor, $borderOpacity, $borderWidth, $borderAnimation,
$rectWidth, $rectHeight, $sort, $bgEffect, $bgStrength,
$vX, $vY, $vW, $vH, $vClip, $vRectWidth, $vRectHeight)
""";
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);
var bgEffectP = cmd.Parameters.Add("$bgEffect", SqliteType.Text);
var bgStrengthP = cmd.Parameters.Add("$bgStrength", SqliteType.Real);
var vXP = cmd.Parameters.Add("$vX", SqliteType.Real);
var vYP = cmd.Parameters.Add("$vY", SqliteType.Real);
var vWP = cmd.Parameters.Add("$vW", SqliteType.Real);
var vHP = cmd.Parameters.Add("$vH", SqliteType.Real);
var vClipP = cmd.Parameters.Add("$vClip", SqliteType.Text);
var vRectWidthP = cmd.Parameters.Add("$vRectWidth", SqliteType.Real);
var vRectHeightP = cmd.Parameters.Add("$vRectHeight", SqliteType.Real);
foreach (var scene in scenes)
{
var z = 0;
foreach (var element in scene.Elements)
{
if (element is not WebcamSceneConfig config) { z++; continue; }
bgEffectP.Value = config.BackgroundEffect.ToString();
bgStrengthP.Value = config.BackgroundStrength;
sceneIdP.Value = scene.Id;
webcamIdP.Value = config.WebcamId;
visibleP.Value = config.IsVisible ? 1 : 0;
opacityP.Value = config.Opacity;
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();
sortP.Value = z++;
// TASK 57: the X/Y/W/H/ClipShape columns stay the 16:9
// layout — while the vertical tier is live the LIVE
// geometry is the vertical placement and the 16:9 one is
// parked in the model — and the Vertical* columns carry
// the vertical placement itself.
if (config.VerticalLayoutLive)
{
config.CommitLiveLayoutToVertical();
xP.Value = config.LandscapeX;
yP.Value = config.LandscapeY;
wP.Value = config.LandscapeWidth;
hP.Value = config.LandscapeHeight;
clipP.Value = config.LandscapeClipShape.ToString();
rectWidthP.Value = (object?)config.LandscapeRectWidth ?? DBNull.Value;
rectHeightP.Value = (object?)config.LandscapeRectHeight ?? DBNull.Value;
}
else
{
xP.Value = config.X;
yP.Value = config.Y;
wP.Value = config.Width;
hP.Value = config.Height;
clipP.Value = config.ClipShape.ToString();
rectWidthP.Value = (object?)config.RectWidth ?? DBNull.Value;
rectHeightP.Value = (object?)config.RectHeight ?? DBNull.Value;
}
vXP.Value = (object?)config.VerticalX ?? DBNull.Value;
vYP.Value = (object?)config.VerticalY ?? DBNull.Value;
vWP.Value = (object?)config.VerticalWidth ?? DBNull.Value;
vHP.Value = (object?)config.VerticalHeight ?? DBNull.Value;
vClipP.Value = (object?)config.VerticalClipShape?.ToString() ?? DBNull.Value;
vRectWidthP.Value = (object?)config.VerticalRectWidth ?? DBNull.Value;
vRectHeightP.Value = (object?)config.VerticalRectHeight ?? DBNull.Value;
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, TextColor, BackgroundEnabled,
BackgroundColor, BackgroundOpacity, TextShadow)
VALUES ($id, $pos, $enabled, $text, $bgen, $bg, $bgopacity, $shadow);
""";
cmd.Transaction = tx;
cmd.Parameters.AddWithValue("$id", socialsId);
cmd.Parameters.AddWithValue("$pos", socials.BarPosition.ToString());
cmd.Parameters.AddWithValue("$enabled", socials.BarEnabled ? 1 : 0);
cmd.Parameters.AddWithValue("$text", socials.BarTextColor);
cmd.Parameters.AddWithValue("$bgen", socials.BarBackgroundEnabled ? 1 : 0);
cmd.Parameters.AddWithValue("$bg", socials.BarBackgroundColor);
cmd.Parameters.AddWithValue("$bgopacity", socials.BarBackgroundOpacity);
cmd.Parameters.AddWithValue("$shadow", socials.BarTextShadow ? 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 ThumbnailAssetId FROM Scene WHERE ThumbnailAssetId IS NOT NULL
UNION
SELECT AlertVideoAssetId FROM Source WHERE AlertVideoAssetId IS NOT NULL
UNION
SELECT Value FROM Settings WHERE Key = 'AlertDefaultVideoAssetId'
UNION
SELECT Value FROM Settings WHERE Key = 'OnAirImageAssetId'
);
""";
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<Source>().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();
}
}