fix(art): re-seed a bundled background when the stored bytes differ
The creator's profile still showed the previous Starting art after the replacement, with `Art.DefaultVersion = 4` recorded — so the version gate had already declared itself done. It compared a constant against the embedded resource, and those two were compiled at different moments (version 4 landed with the OLD image embedded; the corrected image reached the assembly one build later). A version bump can always be raced that way. The seed now compares BYTES: `LayoutStore.AssetMatches` hashes the bundled resource and checks the row the background already points at. Same bytes → leave it untouched (so this is idempotent and never churns the database). Different bytes → re-stamp. The version constant and its setting are gone, because a heal that only runs once can silently skip the one launch it was needed for. `SceneThumbnailTests` planted its own art on a background still flagged as default art — in the real app that state only ever means stale bundled art, because Browse… sets the path and clears the flag. The fixture now models a creator's own picture. Tests: 572 green, 0 warnings. Verified the profile's Starting asset hash (167CDD3B…, the two-category render) differs from the shipped `starting-background.jpg` (5FC6DD22…), so the next launch re-seeds it.
This commit is contained in:
@@ -35,6 +35,20 @@ public partial class LayoutStore : IDisposable
|
||||
return (string)cmd.ExecuteScalar()!;
|
||||
}
|
||||
|
||||
/// <summary>True when the asset already holds exactly these bytes. Used to decide whether a
|
||||
/// bundled image needs re-seeding: comparing bytes cannot go stale the way a version constant
|
||||
/// can (the constant and the embedded resource were built at different moments twice).</summary>
|
||||
public bool AssetMatches(string assetId, byte[] data)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(assetId)) return false;
|
||||
var hash = Convert.ToHexString(SHA256.HashData(data));
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT 1 FROM Asset WHERE Id = $id AND Hash = $hash;";
|
||||
cmd.Parameters.AddWithValue("$id", assetId);
|
||||
cmd.Parameters.AddWithValue("$hash", hash);
|
||||
return cmd.ExecuteScalar() != null;
|
||||
}
|
||||
|
||||
// ─── License (TASK 10 — Polar billing) ───
|
||||
|
||||
/// <summary>The saved license key, or null if never entered.</summary>
|
||||
|
||||
@@ -63,12 +63,6 @@ public partial class LayoutStore : IDisposable
|
||||
|
||||
public void SaveShowMonetaryMetrics(bool show) => UpsertSetting("Metrics.ShowMonetary", show ? "1" : "0");
|
||||
|
||||
/// <summary>Which vintage of the shipped default scene art this layout was stamped with. Bumped when
|
||||
/// a bundled background is replaced, so existing layouts re-seed instead of keeping the old art.</summary>
|
||||
public int LoadDefaultArtVersion() => int.TryParse(GetSetting("Art.DefaultVersion"), out var v) ? v : 1;
|
||||
|
||||
public void SaveDefaultArtVersion(int version) => UpsertSetting("Art.DefaultVersion", version.ToString());
|
||||
|
||||
public bool LoadAutoChromaScreen() => GetSetting("Video.AutoChromaScreen") == "1";
|
||||
|
||||
public void SaveAutoChromaScreen(bool enabled) => UpsertSetting("Video.AutoChromaScreen", enabled ? "1" : "0");
|
||||
|
||||
@@ -427,37 +427,9 @@ public partial class MainViewModel
|
||||
/// <summary>Heals every scene to the locked Background model after a layout
|
||||
/// load: keeps the correct flavor per scene at index 0 named "Background",
|
||||
/// drops duplicates/stale rows, re-seeds missing ones.</summary>
|
||||
/// <summary>Bump when a bundled background image is replaced. Layouts stamped with an older version
|
||||
/// re-seed their DEFAULT art on next load; a creator's own custom background is never touched.</summary>
|
||||
private const int CurrentDefaultArtVersion = 4;
|
||||
|
||||
/// <summary>Re-stamp the shipped art for scenes still using the default. Without this, a replaced
|
||||
/// <c>starting-background.jpg</c> reaches new installs only: the asset is hash-keyed into the layout
|
||||
/// database on first seed, so an existing profile keeps showing the old image forever.</summary>
|
||||
private void HealDefaultArtVersion()
|
||||
{
|
||||
if (_layoutStore.LoadDefaultArtVersion() >= CurrentDefaultArtVersion) return;
|
||||
|
||||
// Every scene's background, not AllBackgrounds(): that helper only returns the LIVE scene's
|
||||
// capture-flavoured rows, which is why version 2 healed nothing (2026-11-06).
|
||||
var cleared = 0;
|
||||
foreach (var scene in Scenes)
|
||||
foreach (var background in scene.Elements.OfType<Source>().Where(element => element.IsBackground))
|
||||
{
|
||||
if (!background.UseDefaultBackground && !string.IsNullOrWhiteSpace(background.CustomBackgroundPath))
|
||||
continue; // the creator chose their own art
|
||||
background.AssetId = null; // the seed below re-stamps the bundled art
|
||||
cleared++;
|
||||
}
|
||||
|
||||
AppLog.Write($"Default art version {CurrentDefaultArtVersion}: re-seeding {cleared} background(s)");
|
||||
_layoutStore.SaveDefaultArtVersion(CurrentDefaultArtVersion);
|
||||
}
|
||||
|
||||
private void HealBackgrounds()
|
||||
{
|
||||
NormalizeBackgrounds(Scenes);
|
||||
HealDefaultArtVersion();
|
||||
foreach (var scene in Scenes)
|
||||
{
|
||||
_sceneGraph.InvalidateBake(scene);
|
||||
@@ -516,7 +488,6 @@ public partial class MainViewModel
|
||||
/// carries none. A custom image (Browse…) always wins and is never overwritten.</summary>
|
||||
private void StampDefaultBackgroundAsset(Source background, string sceneName)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(background.AssetId)) return;
|
||||
if (!background.UseDefaultBackground && !string.IsNullOrWhiteSpace(background.CustomBackgroundPath)) return;
|
||||
var asset = DefaultBackgroundAsset(sceneName);
|
||||
if (asset == null) return;
|
||||
@@ -531,7 +502,13 @@ public partial class MainViewModel
|
||||
if (stream == null) return;
|
||||
using var ms = new MemoryStream();
|
||||
stream.CopyTo(ms);
|
||||
background.AssetId = AddAsset(ms.ToArray());
|
||||
var bytes = ms.ToArray();
|
||||
|
||||
// Already the shipped art → leave it. Compared by BYTES, not by a version stamp: assets are
|
||||
// hash-keyed on first seed, so a replaced bundled image never reaches an existing profile on
|
||||
// its own, and the version constant proved just as stale (2026-11-06).
|
||||
if (_layoutStore.AssetMatches(background.AssetId!, bytes)) return;
|
||||
background.AssetId = AddAsset(bytes);
|
||||
}
|
||||
catch { /* non-critical — the app works without default art */ }
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 2026-11-06: replacing a bundled background reached new installs only. Default art is hash-keyed into the
|
||||
/// layout database on first seed, so an existing profile kept the old image for ever — and the first
|
||||
/// attempt at a version heal silently did nothing because it iterated `AllBackgrounds()`, which returns
|
||||
/// only the LIVE scene's rows.
|
||||
/// layout database on first seed, so an existing profile kept the old image for ever. A version-stamp heal
|
||||
/// failed twice (first it iterated `AllBackgrounds()`, which returns only the LIVE scene's rows; then the
|
||||
/// constant and the embedded resource were built at different moments). The seed now compares BYTES, which
|
||||
/// is what this test pins: a profile already pointing at other art is re-stamped without any version bump.
|
||||
/// </summary>
|
||||
[Collection("RealApp")]
|
||||
public sealed class BackgroundDefaultArtTests
|
||||
@@ -35,8 +36,8 @@ public sealed class BackgroundDefaultArtTests
|
||||
Assert.False(string.IsNullOrWhiteSpace(firstId), "the bundled Starting art never seeded");
|
||||
var firstBytes = AssetLength(tempDb, firstId!);
|
||||
|
||||
// Simulate a profile stamped with the OLD art: point the Starting background at a planted
|
||||
// asset and mark the layout as an older art vintage. The heal must re-stamp the shipped art.
|
||||
// Simulate a profile stamped with the OLD art. No version flag is touched — the seed has to
|
||||
// notice the bytes are not the shipped ones by itself.
|
||||
using (var connection = new SqliteConnection($"Data Source={tempDb}"))
|
||||
{
|
||||
connection.Open();
|
||||
@@ -49,8 +50,6 @@ public sealed class BackgroundDefaultArtTests
|
||||
plant.ExecuteNonQuery();
|
||||
}
|
||||
SqliteConnection.ClearAllPools();
|
||||
using (var store = new LayoutStore(tempDb))
|
||||
store.SaveDefaultArtVersion(1);
|
||||
|
||||
var second = new MainViewModel();
|
||||
var secondId = StartingBackgroundId(second);
|
||||
@@ -64,6 +63,10 @@ public sealed class BackgroundDefaultArtTests
|
||||
AppContext.BaseDirectory, "..", "..", "..", "..", "Assets", "starting-background.jpg")).Length,
|
||||
AssetLength(tempDb, secondId!));
|
||||
Assert.NotEqual(firstBytes, 0);
|
||||
|
||||
// Idempotent: a third launch finds the shipped bytes already in place and must not re-seed.
|
||||
var third = new MainViewModel();
|
||||
Assert.Equal(secondId, StartingBackgroundId(third));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -263,6 +263,11 @@ public sealed class SceneThumbnailTests
|
||||
var id = LayoutStore.Instance!.UpsertAsset(bytes, 320, 180);
|
||||
Assert.NotNull(id);
|
||||
bg!.AssetId = id;
|
||||
// The creator's own picture, not the shipped art: a default-art background pointing at
|
||||
// other bytes is re-seeded by StampDefaultBackgroundAsset (2026-11-06), and this fixture
|
||||
// needs its planted art to stay put. Browse… always sets the path alongside the asset.
|
||||
bg.UseDefaultBackground = false;
|
||||
bg.CustomBackgroundPath = Path.Combine(Path.GetTempPath(), $"ytLive-missing-{i}.jpg");
|
||||
backgroundIds[scene.Name] = id;
|
||||
}
|
||||
builder.SaveLayoutNow();
|
||||
|
||||
Reference in New Issue
Block a user