fix(persist): one shared z-order across Source + WebcamSceneConfig rows

A webcam dragged between sources reverted to the bottom of the stack on
every relaunch: Source and WebcamSceneConfig each carried independent
per-type SortOrder counters, and Load appended all Sources before all
configs. Save now stamps both tables' SortOrder from the element's index
within scene.Elements; Load merges the two tables' rows by that shared z
(sources-first tie-break preserves legacy rows). Cross-type reorder now
survives a fresh LayoutStore reload.

Task 37 queued: defaults vs current layout split (creator directive) —
capture out-of-scope work in TASKS.md rather than folding it in.
This commit is contained in:
2026-09-20 09:23:46 -07:00
parent f91bf8715d
commit 5a1993b6db
7 changed files with 327 additions and 160 deletions
+23 -12
View File
@@ -11,8 +11,8 @@ public partial class LayoutStore : IDisposable
Socials = null;
Music = null;
var scenes = new List<Scene>();
var sourcesByScene = new Dictionary<string, List<Source>>();
var configsByScene = new Dictionary<string, List<WebcamSceneConfig>>();
var sourcesByScene = new Dictionary<string, List<(Source Source, int Sort)>>();
var configsByScene = new Dictionary<string, List<(WebcamSceneConfig Config, int Sort)>>();
using (var cmd = _connection.CreateCommand())
{
@@ -99,7 +99,7 @@ public partial class LayoutStore : IDisposable
IsBackground, CaptureKey, ShowDesktop,
ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages,
ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor,
WebUri, MediaPath, MediaIsLooping, MediaVolume, MediaPlaybackState
WebUri, MediaPath, MediaIsLooping, MediaVolume, MediaPlaybackState, SortOrder
FROM Source ORDER BY SortOrder
""";
using var reader = cmd.ExecuteReader();
@@ -147,8 +147,8 @@ public partial class LayoutStore : IDisposable
: MediaPlaybackState.Stopped,
};
if (!sourcesByScene.TryGetValue(sceneId, out var list))
sourcesByScene[sceneId] = list = new List<Source>();
list.Add(source);
sourcesByScene[sceneId] = list = new List<(Source, int)>();
list.Add((source, reader.GetInt32(30)));
}
}
@@ -157,7 +157,7 @@ public partial class LayoutStore : IDisposable
cmd.CommandText = """
SELECT SceneId, WebcamId, IsVisible, X, Y, Width, Height, Opacity,
ClipShape, IsMirrored, BorderColor, BorderOpacity, BorderWidth, BorderAnimation,
RectWidth, RectHeight
RectWidth, RectHeight, SortOrder
FROM WebcamSceneConfig ORDER BY SortOrder
""";
using var reader = cmd.ExecuteReader();
@@ -184,19 +184,30 @@ public partial class LayoutStore : IDisposable
RectHeight = reader.IsDBNull(15) ? null : reader.GetDouble(15),
};
if (!configsByScene.TryGetValue(sceneId, out var list))
configsByScene[sceneId] = list = new List<WebcamSceneConfig>();
list.Add(config);
configsByScene[sceneId] = list = new List<(WebcamSceneConfig, int)>();
list.Add((config, reader.GetInt32(16)));
}
}
foreach (var scene in scenes)
{
// Source and WebcamSceneConfig rows share ONE z-order space per scene
// (both SortOrders = the element's index within scene.Elements). Merge
// the two tables' rows by that shared z rather than appending all
// sources before all configs — a webcam slotted between sources must
// reload in place. Legacy rows (written before the unified space) may
// tie at 0: break ties sources-first, so old data reads back stacked
// the same way it did pre-unification.
var ordered = new List<(int Sort, SceneElement Element)>();
if (sourcesByScene.TryGetValue(scene.Id, out var sources))
foreach (var source in sources)
scene.Elements.Add(source);
foreach (var (source, sort) in sources)
ordered.Add((sort, source));
if (configsByScene.TryGetValue(scene.Id, out var configs))
foreach (var config in configs)
scene.Elements.Add(config);
foreach (var (config, sort) in configs)
ordered.Add((sort, config));
foreach (var (_, element) in ordered.OrderBy(e => e.Sort).ThenBy(e => e.Element is Source ? 0 : 1))
scene.Elements.Add(element);
var bg = scene.Elements.OfType<Source>().FirstOrDefault(s => s.IsBackground);
if (bg != null)