Scene strip tile artwork + on-air indicator (creator ruling 2026-10-10). Top-bar tiles now carry their own art: each canonical scene's tile is a per-scene CRT-styled image stored in the internal DB (Scene.ThumbnailAssetId - additive migration, seeded at heal from the creator's five Downloads JPGs via SceneThumbnailSeedPaths), and the preview pane keeps rendering the scene's ORIGINAL background - never the tiles. One shared on-air image (Gemini 4zajyl...jpg) replaces the staged scene's tile and follows the scene swap ('the on-air screen jumps around the top bar as the user swaps scenes'): stored as an Asset BLOB tracked by the OnAirImageAssetId Setting (EnsureOnAirImage, same contract as the alert clip) with MainViewModel.OnAirSeedPath override; RefreshSnapshotsAsync shows it for the staged scene, otherwise thumbnail-or-background art, and a staged Live with no on-air art still falls back to the green placeholder (minis never render live captures). Removed the 6px #22c55e rectangle in PreviewPane's backdrop ('green outline around the preview screen' that surfaced when a scene had no background) - the outer PreviewGlow red border is the real live signal. Doubled the scene-tile labels (FontSize 10 -> 20). Fixed a real bug this surfaced: Save()'s Asset pruner only kept Source/alert-clip rows, so the first save would have DELETED the seeded tile + on-air assets - the keep-set now includes Scene.ThumbnailAssetId and the OnAirImageAssetId Setting. New Settings/Load/Save migrations + SceneThumbnailTests (4 tests: seeded tile in strip, preview keeps original bg, Live-staged tile, no-seed fallback, on-air swap + persistence). 427 pass, 0 build warnings. (Unit: strip tiles + on-air)

This commit is contained in:
2026-10-10 13:27:26 -07:00
parent 06f88f4142
commit 8e1d6f44db
12 changed files with 576 additions and 61 deletions
+2 -1
View File
@@ -16,7 +16,7 @@ public partial class LayoutStore : IDisposable
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = "SELECT Id, Name, IsHidden, IsChatScene, HasBackground, HasSocialBar FROM Scene ORDER BY SortOrder";
cmd.CommandText = "SELECT Id, Name, IsHidden, IsChatScene, HasBackground, HasSocialBar, ThumbnailAssetId FROM Scene ORDER BY SortOrder";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
@@ -28,6 +28,7 @@ public partial class LayoutStore : IDisposable
IsChatScene = reader.GetInt32(3) != 0,
HasBackground = reader.GetInt32(4) != 0,
HasSocialBar = reader.FieldCount > 5 && !reader.IsDBNull(5) && reader.GetInt32(5) != 0,
ThumbnailAssetId = reader.FieldCount > 6 && !reader.IsDBNull(6) ? reader.GetString(6) : null,
});
}
}
+22
View File
@@ -138,6 +138,7 @@ public partial class LayoutStore : IDisposable
MigrateWebcamConfigTable();
MigrateSceneTable();
MigrateSceneSocialBarColumn();
MigrateSceneThumbnailColumn();
MigrateSocialsTable();
MigrateSocialEntryTable();
if (GetUserVersion() < 3)
@@ -527,6 +528,27 @@ public partial class LayoutStore : IDisposable
alter.ExecuteNonQuery();
}
// 2026-10-10: Scene gains ThumbnailAssetId (the top-strip tile artwork, stored
// as an Asset row like every image). Additive, no version bump — old DBs just
// get a NULL column and the strip falls back to the scene's background art.
private void MigrateSceneThumbnailColumn()
{
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("ThumbnailAssetId")) return;
using var alter = _connection.CreateCommand();
alter.CommandText = "ALTER TABLE Scene ADD COLUMN ThumbnailAssetId TEXT;";
alter.ExecuteNonQuery();
}
// v7 → v8: Socials gains BarEnabled (the dialog's show/hide switch). The old
// BarJustify column stays for back-compat but is no longer read or written.
private void MigrateSocialsTable()
+8 -2
View File
@@ -54,8 +54,8 @@ public partial class LayoutStore : IDisposable
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)
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);
@@ -64,6 +64,7 @@ public partial class LayoutStore : IDisposable
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;
@@ -75,6 +76,7 @@ public partial class LayoutStore : IDisposable
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();
}
@@ -324,9 +326,13 @@ public partial class LayoutStore : IDisposable
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;
+22
View File
@@ -326,4 +326,26 @@ public partial class LayoutStore : IDisposable
/// <summary>The stored default clip's Asset id, or null when never stamped.</summary>
public string? GetAlertDefaultVideoId() => GetSetting(AlertDefaultVideoKey);
internal const string OnAirImageKey = "OnAirImageAssetId";
/// <summary>The strip tile that labels the scene currently PREVIEWED (creator
/// ruling 2026-10-10 — the "on-air background screen"). One shared image, not
/// per-scene: whichever scene is staged shows it in the top bar, so it "jumps
/// around" as the creator swaps scenes. Lives as a BLOB in the internal Asset
/// table, tracked by this Setting so the Save pruner keeps it alive — the same
/// contract as the alert default clip. <c>UpsertAsset</c> is hash-keyed, so
/// re-stamping the same bytes returns the same id.</summary>
public string EnsureOnAirImage(byte[] data)
{
var saved = GetSetting(OnAirImageKey);
if (!string.IsNullOrWhiteSpace(saved) && GetAssetBytes(saved) != null)
return saved;
var id = UpsertAsset(data, 0, 0);
UpsertSetting(OnAirImageKey, id);
return id;
}
/// <summary>The stored on-air image's Asset id, or null when never stamped.</summary>
public string? GetOnAirImageId() => GetSetting(OnAirImageKey);
}