art(starting): the corrected onboarding screen, and a heal that actually heals
The creator's corrected Starting artwork (the in-app onboarding screen: sign in → stream details → go live) replaces yesterday's version. 2724x1536 resampled to the 1920x1080 master, q3 (432 KB). The previous Gemini render is archived beside the original art. Also fixes the two reasons the replacement would not have appeared on an existing profile: - **The first heal iterated `AllBackgrounds()`**, which returns only the LIVE scene's capture rows, so it cleared nothing and then saved the version anyway — permanently skipping the heal. It now walks every scene's background. Version bumped to 4 so profiles stamped 2 or 3 heal. - **The pack URI was unqualified**, so it resolved against the ENTRY assembly: LlamaCasty.exe in production, the test host under `dotnet test`, where the seed silently did nothing. It is now `/LlamaCasty;component/Assets/…`, which works in both. Test: `BackgroundDefaultArtTests` seeds a profile, plants an "old art" asset on the Starting background, marks the layout as an older art vintage, relaunches and asserts the shipped art is re-stamped. 45 background tests pass.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 423 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 423 KiB After Width: | Height: | Size: 422 KiB |
@@ -429,7 +429,7 @@ public partial class MainViewModel
|
||||
/// 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 = 2;
|
||||
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
|
||||
@@ -438,12 +438,19 @@ public partial class MainViewModel
|
||||
{
|
||||
if (_layoutStore.LoadDefaultArtVersion() >= CurrentDefaultArtVersion) return;
|
||||
|
||||
foreach (var background in AllBackgrounds())
|
||||
// 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) continue; // the creator chose their own art
|
||||
background.AssetId = null; // seed re-stamps it below
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -516,7 +523,10 @@ public partial class MainViewModel
|
||||
|
||||
try
|
||||
{
|
||||
var uri = new Uri($"pack://application:,,,/Assets/{asset}", UriKind.Absolute);
|
||||
// Assembly-qualified on purpose: the unqualified form resolves against the ENTRY assembly,
|
||||
// which is LlamaCasty.exe in production but the test host under `dotnet test` — so the seed
|
||||
// silently did nothing there (and the art test could not see the bundled image).
|
||||
var uri = new Uri($"pack://application:,,,/LlamaCasty;component/Assets/{asset}", UriKind.Absolute);
|
||||
using var stream = Application.GetResourceStream(uri)?.Stream;
|
||||
if (stream == null) return;
|
||||
using var ms = new MemoryStream();
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Xunit;
|
||||
using ytLive.Services;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
[Collection("RealApp")]
|
||||
public sealed class BackgroundDefaultArtTests
|
||||
{
|
||||
private readonly RealAppHost _app;
|
||||
|
||||
public BackgroundDefaultArtTests(RealAppHost app) => _app = app;
|
||||
|
||||
[Fact]
|
||||
public void ReplacingTheBundledArt_ReSeedsAnExistingProfile() => _app.Run(() =>
|
||||
{
|
||||
// The bundled art is a WPF resource, so this needs a real Application to resolve the pack URI.
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-art-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
try
|
||||
{
|
||||
// First launch: the seeded art is whatever ships today.
|
||||
var first = new MainViewModel();
|
||||
var firstId = StartingBackgroundId(first);
|
||||
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.
|
||||
using (var connection = new SqliteConnection($"Data Source={tempDb}"))
|
||||
{
|
||||
connection.Open();
|
||||
using var plant = connection.CreateCommand();
|
||||
plant.CommandText = """
|
||||
INSERT OR REPLACE INTO Asset (Id, Hash, Data, PixelWidth, PixelHeight)
|
||||
VALUES ('old-art', 'deadbeef', zeroblob(1024), 1920, 1080);
|
||||
UPDATE Source SET AssetId = 'old-art' WHERE IsBackground = 1;
|
||||
""";
|
||||
plant.ExecuteNonQuery();
|
||||
}
|
||||
SqliteConnection.ClearAllPools();
|
||||
using (var store = new LayoutStore(tempDb))
|
||||
store.SaveDefaultArtVersion(1);
|
||||
|
||||
var second = new MainViewModel();
|
||||
var secondId = StartingBackgroundId(second);
|
||||
Assert.NotNull(secondId);
|
||||
|
||||
// Re-stamped to the SHIPPED art — which, being the same bytes, is the same asset row the
|
||||
// first launch created. What matters is that the planted "old art" is gone.
|
||||
Assert.NotEqual("old-art", secondId);
|
||||
Assert.Equal(firstId, secondId);
|
||||
Assert.Equal(File.ReadAllBytes(Path.Combine(
|
||||
AppContext.BaseDirectory, "..", "..", "..", "..", "Assets", "starting-background.jpg")).Length,
|
||||
AssetLength(tempDb, secondId!));
|
||||
Assert.NotEqual(firstBytes, 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
});
|
||||
|
||||
private static string? StartingBackgroundId(MainViewModel vm)
|
||||
=> vm.Scenes
|
||||
.Where(scene => ytLive.Models.SceneCatalog.Is(scene.Name, ytLive.Models.SceneCatalog.Starting))
|
||||
.SelectMany(scene => scene.Elements.OfType<ytLive.Models.Source>())
|
||||
.FirstOrDefault(element => element.IsBackground)?.AssetId;
|
||||
|
||||
private static int AssetLength(string dbPath, string assetId)
|
||||
{
|
||||
using var connection = new SqliteConnection($"Data Source={dbPath}");
|
||||
connection.Open();
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT length(Data) FROM Asset WHERE Id = $id;";
|
||||
cmd.Parameters.AddWithValue("$id", assetId);
|
||||
return Convert.ToInt32(cmd.ExecuteScalar());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user