Creator request 2026-11-01: a layer that hides part of the desktop from viewers. Box or circle, drag/resize like any layer, one strength slider, several allowed. **Z-order is policy, not preference.** `AddSource` inserts a blur directly above the background and therefore below the brand logo, and `OnSceneElementsReordered` pushes any blur that ends up above the brand back down. Nothing else in the app can move it into the brand's way. **Overlap = the strongest single blur, never a stack.** `SceneCompositor.ApplyBlurLayers` collects every visible blur the first time one is reached in the z-order, sorts them strongest-first, and shares one coverage map: a pixel already covered by a stronger blur is skipped. Blurring back-to-front would have produced something stronger than any slider setting allows, which is exactly what was ruled out. **One blur curve, shared with the webcam.** `RegionBlur` carries the constants the webcam blur already used — downscale 4, radius 1–6 downscaled px, `RadiusFor(strength)` — and `BackgroundEffectProcessor` now delegates to it instead of holding its own copy. The layer's slider is the same 0–1 scale with the same 0.5 default, so "half strength" means the same blur in both features. Cost is proportional to the blur's own rect, not the frame. **Live scene only**, enforced in `AddSource` as well as the (+) menu (`CanAddBlur`), so a stale menu item or a scripted call cannot plant one on BRB. `ElementKind.Dynamic`, so it is never baked into the static base and every render path gets it — pump, preview, thumbnails, scene dump. Persistence: `Source.BlurAmount` column + migration; shape rides the existing `ClipShape`. Properties panel: Box | Circle plus the strength slider, gated on `IsBlurSelected`. 4 tests: outside the mask is byte-identical, inside loses its hard edges, a circle leaves its box corners alone, and two identical stacked blurs equal one (the coverage map). Release builds at 0 warnings.
431 lines
24 KiB
C#
431 lines
24 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, SortOrder)
|
|
VALUES ($id, $name, $isHidden, $isChat, $hasBackground, $hasSocialBar, $thumbnail, $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 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;
|
|
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)
|
|
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)
|
|
""";
|
|
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);
|
|
var isBrandLogoP = cmd.Parameters.Add("$isBrandLogo", SqliteType.Integer);
|
|
cmd.Parameters.Add("$blurAmount", SqliteType.Real);
|
|
|
|
// 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;
|
|
isBrandLogoP.Value = source.IsBrandLogo ? 1 : 0;
|
|
cmd.Parameters["$blurAmount"].Value = source.BlurAmount;
|
|
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();
|
|
}
|
|
}
|