TASK 21: real logo + creator-hub About overlay — the About overlay is now the in-app creator hub: real logo (the creator's 'llama fortnite superman logo' copied to Assets/llama-logo.png, creator-owned art), hub links — llama chile shop on YouTube (MainViewModel.ChannelUrl, now public), Mastodon (https://mastodon.llamachile.tube/@gramps), Buy me a coffee (https://buymeacoffee.com/llamachiley, live), Unlock Premium (greyed 'coming soon'; IsPremiumAvailable flips on once the tabled itch.io PremiumUrl lands); a Licenses & legal sub-panel loads the full shipped THIRD-PARTY-NOTICES.txt in-app (ShowLicenses reads from the executable dir on first open, 'not found' fallback — never the OS viewer) with '← Back to About' (BackToAbout_Click) — the AboutHubTests integration test drives the real window + VM asserting the hub opens, link URLs are real, licensing loads the shipped notices text, and back returns to the hub — 174 tests passing, 0 warnings

This commit is contained in:
2026-08-14 12:39:05 -07:00
parent 801219ec46
commit 3d92bd0225
9 changed files with 225 additions and 32 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

+23 -10
View File
@@ -8,6 +8,18 @@
## Session state (last updated: 2026-08-14) ## Session state (last updated: 2026-08-14)
- **Branch:** `main`, in sync with `origin/main`. - **Branch:** `main`, in sync with `origin/main`.
- **TASK 21 — real logo + creator-hub About: SHIPPED** (this session). The About overlay is now the
**creator hub**: the real logo ("llama fortnite superman logo" copied from `\\llamavault``Assets/llama-logo.png`,
the creator's own art — no third-party license), plus hub links — **llama chile shop on YouTube**
(`MainViewModel.ChannelUrl`, public now), **Mastodon** (`https://mastodon.llamachile.tube/@gramps`),
**Buy me a coffee** (`https://buymeacoffee.com/llamachiley` — LIVE), and **Unlock Premium** (greyed
"coming soon"; the itch.io product URL is a **tabled seam**, `MainViewModel.PremiumUrl` — the itch.io
account was just created and needs a setup session later). **Licenses & legal** flips the About overlay
to an in-app scrolling panel loading the shipped `THIRD-PARTY-NOTICES.txt` (`ShowLicenses()` reads from
the executable dir on first open, graceful "not found" fallback — never the OS viewer); "← Back to
About" (`BackToAbout_Click`) returns. Integration test (ONE): `AboutHubTests` drives the real window,
asserting the hub opens, the link URLs are real, licensing loads the shipped notices text, and back
returns to the hub. **174 tests passing, 0 warnings.**
- **TASK 8 — meter scaling amplification:** SHIPPED + pushed (`0b71b03`). `AudioLevelMeter.ToDisplay` - **TASK 8 — meter scaling amplification:** SHIPPED + pushed (`0b71b03`). `AudioLevelMeter.ToDisplay`
adds **+10 dB input amplification** before the 60..0 dBFS log map: speech peaks (~0.2 RMS) read adds **+10 dB input amplification** before the 60..0 dBFS log map: speech peaks (~0.2 RMS) read
~0.93 (red) and normal speech (~0.05) ~0.73 (yellow) at maxed volume; ≤0.001 linear still reads 0 ~0.93 (red) and normal speech (~0.05) ~0.73 (yellow) at maxed volume; ≤0.001 linear still reads 0
@@ -26,17 +38,18 @@
IsLivePrivate}"` (MainWindow.xaml ~110). IsLivePrivate}"` (MainWindow.xaml ~110).
5. **Integration test** — `ytLive.Tests/YouTubeStreamServiceTests.cs`: the broadcast-insert request 5. **Integration test** — `ytLive.Tests/YouTubeStreamServiceTests.cs`: the broadcast-insert request
must carry `"privacyStatus":"private"` (RecordingHandler seam) + a no-session → null guard test. must carry `"privacyStatus":"private"` (RecordingHandler seam) + a no-session → null guard test.
- **Verified:** build 0 warnings / 0 errors; **173/173 tests pass** (171 + the 2 stream-service tests). - **Verified:** build 0 warnings / 0 errors; **174/174 tests pass** (173 + the new AboutHubTests).
- **Uncommitted (this session's ship step 7 batch):** `GoLiveWindow.xaml`, `ViewModels/GoLiveViewModel.cs`, - **Uncommitted (this session's TASK 21 batch):** `MainWindow.xaml`, `ViewModels/MainViewModel.cs`,
`ViewModels/MainViewModel.cs`, `Services/YouTubeStreamService.cs`, `MainWindow.xaml`, `MainWindow.xaml.cs`, `Assets/llama-logo.png` (replaced with the real logo), `ytLive.Tests/AboutHubTests.cs`
`ytLive.Tests/YouTubeStreamServiceTests.cs` (new), `TASKS.md`, `ai.md`, `ViewModels/index.md`, (new), `TASKS.md`, `HANDOFF.md`. ai.md + ViewModels/index.md updates pending — **commit + push
`Services/index.md`, `HANDOFF.md`. **Commit + push before continuing.** before continuing.**
- **Next step:** commit + push ship step 7, then **TASK 5 — reusable `variable` stream** (create once - **Next step:** commit + push TASK 21, then **TASK 5 — reusable `variable` stream** (create once
per channel, cache ingestion URL, bind to broadcast, feed `_rtmpUrlProvider` so the pump actually per channel, cache ingestion URL, bind to broadcast, feed `_rtmpUrlProvider` so the pump actually
pushes — `_currentBroadcastId` is already stashed for it). Also queued: task 21 (logo + About hub), pushes — `_currentBroadcastId` is already stashed for it). Also queued: **task 22 (voice filters)**,
task 22 (voice filters). Nothing else queued — do not expand the task queue on your own. Optional, and **setting up the itch.io product page** (the creator just made the account; the premium-unlock
not queued: rewriting the healed entry's `ProfileUrl` to `https://mastodon.llamachile.tube/@gramps` URL then lands in `MainViewModel.PremiumUrl` and the About hub's "Unlock Premium" button lights up).
(user must say the word). Nothing else queued — do not expand the task queue on your own. Optional, not queued: rewriting the
healed entry's `ProfileUrl` to `https://mastodon.llamachile.tube/@gramps` (user must say the word).
- **Landmines:** - **Landmines:**
- Never add another test that constructs `new App()` — use `RealAppHost.Run(...)` (shared STA host - Never add another test that constructs `new App()` — use `RealAppHost.Run(...)` (shared STA host
+41 -3
View File
@@ -1110,8 +1110,11 @@
</StackPanel> </StackPanel>
<!-- About --> <!-- About -->
<StackPanel Grid.Row="1" Margin="0,16,0,0" <Grid Grid.Row="1" Margin="0,16,0,0"
Visibility="{Binding IsAboutOpen, Converter={StaticResource BoolToVis}}"> Visibility="{Binding IsAboutOpen, Converter={StaticResource BoolToVis}}">
<!-- Hub view -->
<StackPanel Visibility="{Binding IsLicensesOpen, Converter={StaticResource InverseBoolToVis}}">
<Image Source="/ytLive;component/Assets/llama-logo.png" Height="140" Stretch="Uniform" <Image Source="/ytLive;component/Assets/llama-logo.png" Height="140" Stretch="Uniform"
HorizontalAlignment="Center" Margin="0,0,0,12"/> HorizontalAlignment="Center" Margin="0,0,0,12"/>
<TextBlock Text="ytLlive" FontSize="22" FontWeight="Bold" Foreground="#e94560" <TextBlock Text="ytLlive" FontSize="22" FontWeight="Bold" Foreground="#e94560"
@@ -1126,9 +1129,44 @@
TextAlignment="Center" Margin="0,12,0,0"/> TextAlignment="Center" Margin="0,12,0,0"/>
<TextBlock Text="Made by gramps · llama chile shop" Foreground="#a0a0b0" FontSize="12" <TextBlock Text="Made by gramps · llama chile shop" Foreground="#a0a0b0" FontSize="12"
HorizontalAlignment="Center" Margin="0,16,0,8"/> HorizontalAlignment="Center" Margin="0,16,0,8"/>
<Button Content="Visit llama chile shop on YouTube" Command="{Binding OpenChannelCommand}"
Style="{StaticResource YtButtonSecondary}" HorizontalAlignment="Center" Margin="0,0,0,4"/> <!-- Creator hub links -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,8">
<Button Content="llama chile shop on YouTube" Command="{Binding OpenChannelCommand}"
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
<Button Content="Mastodon" Command="{Binding OpenMastodonCommand}"
Style="{StaticResource YtButtonSecondary}"/>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="Buy me a coffee" Command="{Binding OpenCoffeeCommand}"
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
<Button Content="Unlock Premium" Command="{Binding OpenPremiumCommand}"
Style="{StaticResource YtButtonSecondary}"
IsEnabled="{Binding IsPremiumAvailable}"
ToolTip="Coming soon — the itch.io unlock page is being set up"/>
</StackPanel>
<Button Content="Licenses &amp; legal" Command="{Binding OpenLicensesCommand}"
Style="{StaticResource YtButtonSecondary}" HorizontalAlignment="Center"
Margin="0,16,0,0"/>
</StackPanel>
<!-- Licensing/legal panel -->
<StackPanel Visibility="{Binding IsLicensesOpen, Converter={StaticResource BoolToVis}}">
<TextBlock Text="Third-party notices &amp; license texts" FontSize="15" FontWeight="Bold"
Foreground="#e0e0e0" Margin="0,0,0,8"/>
<Border Background="#10101e" CornerRadius="4" Padding="10" MaxHeight="360"
BorderBrush="#333" BorderThickness="1">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBlock Text="{Binding LicensesText}" Foreground="#b8b8c8" FontSize="11"
FontFamily="Consolas" TextWrapping="Wrap"/>
</ScrollViewer>
</Border>
<Button Content="← Back to About" Style="{StaticResource YtButtonSecondary}"
HorizontalAlignment="Left" Margin="0,12,0,0"
Click="BackToAbout_Click"/>
</StackPanel>
</Grid>
</Grid> </Grid>
</Border> </Border>
</Grid> </Grid>
+6
View File
@@ -143,6 +143,12 @@ public partial class MainWindow : Window
} }
} }
private void BackToAbout_Click(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MainViewModel vm)
vm.IsLicensesOpen = false;
}
private void AddSourceButton_Click(object sender, RoutedEventArgs e) private void AddSourceButton_Click(object sender, RoutedEventArgs e)
{ {
if (sender is Button { ContextMenu: { } menu } button) if (sender is Button { ContextMenu: { } menu } button)
+1 -1
View File
@@ -115,7 +115,7 @@ Lifecycle: `created → ready → [testing] → live → complete` (transitional
18.**Chat box** — YouTube live chat rendered *on* the stream so viewers read along in-video 18.**Chat box** — YouTube live chat rendered *on* the stream so viewers read along in-video
19.**Background removal (milestone 2)** — ONNX Runtime + DirectML, MediaPipe Selfie Segmentation — deliberately NOT in this build 19.**Background removal (milestone 2)** — ONNX Runtime + DirectML, MediaPipe Selfie Segmentation — deliberately NOT in this build
20.**Alerts** — Super Chat / membership / subscribe pop-ins; build after the six; **the one paid feature** (see Monetization in `ai.md`) 20.**Alerts** — Super Chat / membership / subscribe pop-ins; build after the six; **the one paid feature** (see Monetization in `ai.md`)
21. **Logo + richer in-app About (2026-08-13, queued)** — the ytLlive wordmark in the top bar is clickable and opens the About overlay; make a real **logo** (CC0 per the icon guardrail), and expand the About overlay into the creator's hub: **links to premium unlock + coffee/thanks buys + socials** and a second (or third) **scrolling panel with the full licensing/legal texts on demand** — everything stays IN-APP (no Notepad, no browser tab; the app is standalone). The top-bar About button that opened `THIRD-PARTY-NOTICES.txt` in the OS viewer was removed on 2026-08-13 for exactly this reason 21. **Logo + richer in-app About (2026-08-13, queued → 2026-08-14 SHIPPED)** — the ytLlive wordmark in the top bar opens the About overlay (already wired); the About overlay is now the **creator hub**: the real logo (the "llama fortnite superman logo" from the creator's vault, copied to `Assets/llama-logo.png` — the creator's own art, no third-party license), plus **creator-hub links** — llama chile shop on YouTube (`MainViewModel.ChannelUrl`), Mastodon (`https://mastodon.llamachile.tube/@gramps`), **Buy me a coffee** (`https://buymeacoffee.com/llamachiley` — live), and **Unlock Premium** (greyed "coming soon" — the itch.io product URL is a tabled seam, `PremiumUrl`, until the account is set up). A **Licenses & legal** button flips the About overlay to an in-app scrolling panel that loads the full `THIRD-PARTY-NOTICES.txt` text (`MainViewModel.ShowLicenses` reads the shipped file from the executable directory on first open; graceful "not found" fallback — **never the OS viewer, everything stays in-app**); "← Back to About" returns. The About overlay is also the in-app home of the notices — the top-bar About button that opened the file in the OS viewer was removed on 2026-08-13 for exactly this reason. Integration test (Good Dog Rule — ONE): `AboutHubTests.About_Opens_InApp_Licensing_Loads_Shipped_Notices` drives the real window + VM, asserting the hub opens, the link URLs are real, the licensing panel loads the shipped notices text (contains "Third-Party Notices" + "LGPL"), and back returns to the hub. 174 tests passing, 0 warnings
22.**Voice filters on the mic channel (2026-08-13, queued — KISS, always on)** — the standard four applied to the sound input path (before the meter/encoder mix): **bass boost, treble, noise suppression, compressor** (set decided with the creator 2026-08-13). Always-on — no UI knobs; the mic stays the creator's single audio control 22.**Voice filters on the mic channel (2026-08-13, queued — KISS, always on)** — the standard four applied to the sound input path (before the meter/encoder mix): **bass boost, treble, noise suppression, compressor** (set decided with the creator 2026-08-13). Always-on — no UI knobs; the mic stays the creator's single audio control
### The Minimal Source Set (design decision — do not expand casually) ### The Minimal Source Set (design decision — do not expand casually)
+58 -1
View File
@@ -69,6 +69,7 @@ public class MainViewModel : ViewModelBase
private bool _isBugOpen; private bool _isBugOpen;
private bool _isFeatureOpen; private bool _isFeatureOpen;
private bool _isAboutOpen; private bool _isAboutOpen;
private bool _isLicensesOpen;
private string _overlayTitle = string.Empty; private string _overlayTitle = string.Empty;
private string _defaultStreamTitle = string.Empty; private string _defaultStreamTitle = string.Empty;
private string _defaultStreamDescription = string.Empty; private string _defaultStreamDescription = string.Empty;
@@ -128,9 +129,24 @@ public class MainViewModel : ViewModelBase
private bool _brandFlashActive; private bool _brandFlashActive;
private const string SupportEmail = "gramps@llamachile.shop"; private const string SupportEmail = "gramps@llamachile.shop";
private const string ChannelUrl = "https://youtube.com/@llamachileshop"; // The creator-hub link targets (About overlay) — public so the hub's
// integration test can assert they point at real destinations.
public const string ChannelUrl = "https://youtube.com/@llamachileshop";
public const string MastodonUrl = "https://mastodon.llamachile.tube/@gramps";
// Tabled 2026-08-14: the itch.io account was just created — the product page
// (premium unlock) lands here once set up. Coffee/thanks URL is live.
public const string PremiumUrl = "";
public const string CoffeeUrl = "https://buymeacoffee.com/llamachiley";
public static string AppVersionLabel => $"Version {typeof(MainViewModel).Assembly.GetName().Version?.ToString(3)}"; public static string AppVersionLabel => $"Version {typeof(MainViewModel).Assembly.GetName().Version?.ToString(3)}";
/// <summary>The About hub's Unlock Premium button lights up only once the
/// itch.io product URL is set (tabled 2026-08-14 — account in setup).</summary>
public bool IsPremiumAvailable => PremiumUrl.Length > 0;
/// <summary>Full in-app licensing/legal text, loaded on demand from the
/// shipped THIRD-PARTY-NOTICES.txt (never opens the OS viewer).</summary>
public string LicensesText { get; private set; } = string.Empty;
public ObservableCollection<Scene> Scenes { get; } = new(); public ObservableCollection<Scene> Scenes { get; } = new();
public ObservableCollection<ChatMessage> ChatMessages { get; } = new(); public ObservableCollection<ChatMessage> ChatMessages { get; } = new();
public ObservableCollection<DisplayInfo> Displays { get; } = new(); public ObservableCollection<DisplayInfo> Displays { get; } = new();
@@ -743,6 +759,14 @@ public class MainViewModel : ViewModelBase
private set => SetPanel(ref _isAboutOpen, value); private set => SetPanel(ref _isAboutOpen, value);
} }
/// <summary>About hub sub-view: the full licensing/legal texts panel
/// (in-app scrolling — no Notepad, no browser tab).</summary>
public bool IsLicensesOpen
{
get => _isLicensesOpen;
set => SetProperty(ref _isLicensesOpen, value);
}
public bool IsAnyOverlayOpen => IsSettingsOpen || IsBugOpen || IsFeatureOpen || IsAboutOpen; public bool IsAnyOverlayOpen => IsSettingsOpen || IsBugOpen || IsFeatureOpen || IsAboutOpen;
public string OverlayTitle public string OverlayTitle
@@ -949,6 +973,10 @@ public class MainViewModel : ViewModelBase
public ICommand SubmitBugCommand { get; } public ICommand SubmitBugCommand { get; }
public ICommand SubmitFeatureCommand { get; } public ICommand SubmitFeatureCommand { get; }
public ICommand OpenChannelCommand { get; } public ICommand OpenChannelCommand { get; }
public ICommand OpenMastodonCommand { get; }
public ICommand OpenCoffeeCommand { get; }
public ICommand OpenPremiumCommand { get; }
public ICommand OpenLicensesCommand { get; }
public ICommand SaveLayoutCommand { get; } public ICommand SaveLayoutCommand { get; }
public ICommand SaveLayoutAsCommand { get; } public ICommand SaveLayoutAsCommand { get; }
public ICommand OpenLayoutCommand { get; } public ICommand OpenLayoutCommand { get; }
@@ -1010,6 +1038,10 @@ public class MainViewModel : ViewModelBase
SubmitBugCommand = new RelayCommand(_ => SubmitBug()); SubmitBugCommand = new RelayCommand(_ => SubmitBug());
SubmitFeatureCommand = new RelayCommand(_ => SubmitFeature()); SubmitFeatureCommand = new RelayCommand(_ => SubmitFeature());
OpenChannelCommand = new RelayCommand(_ => OpenUrl(ChannelUrl)); OpenChannelCommand = new RelayCommand(_ => OpenUrl(ChannelUrl));
OpenMastodonCommand = new RelayCommand(_ => OpenUrl(MastodonUrl));
OpenCoffeeCommand = new RelayCommand(_ => OpenUrl(CoffeeUrl));
OpenPremiumCommand = new RelayCommand(_ => OpenUrl(PremiumUrl), _ => IsPremiumAvailable);
OpenLicensesCommand = new RelayCommand(_ => ShowLicenses());
StartStreamCommand = new RelayCommand(_ => BeginGoLive()); StartStreamCommand = new RelayCommand(_ => BeginGoLive());
EndStreamCommand = new RelayCommand(_ => StopStream(), _ => IsLive); EndStreamCommand = new RelayCommand(_ => StopStream(), _ => IsLive);
SaveLayoutCommand = new RelayCommand(_ => SaveLayoutNow()); SaveLayoutCommand = new RelayCommand(_ => SaveLayoutNow());
@@ -1869,6 +1901,31 @@ public class MainViewModel : ViewModelBase
IsFeatureOpen = panel == nameof(IsFeatureOpen); IsFeatureOpen = panel == nameof(IsFeatureOpen);
IsAboutOpen = panel == nameof(IsAboutOpen); IsAboutOpen = panel == nameof(IsAboutOpen);
OverlayTitle = title; OverlayTitle = title;
if (panel != nameof(IsAboutOpen))
IsLicensesOpen = false;
}
/// <summary>Shows the in-app licensing panel, loading the full texts from
/// the shipped THIRD-PARTY-NOTICES.txt on first open (never the OS viewer).
/// Falls back to a "not found" note if the file is missing so the panel
/// never hangs on nothing.</summary>
private void ShowLicenses()
{
if (string.IsNullOrEmpty(LicensesText))
{
try
{
var path = Path.Combine(AppContext.BaseDirectory, "THIRD-PARTY-NOTICES.txt");
LicensesText = File.Exists(path)
? File.ReadAllText(path)
: "THIRD-PARTY-NOTICES.txt not found next to the executable.";
}
catch (Exception ex)
{
LicensesText = $"Could not read THIRD-PARTY-NOTICES.txt: {ex.Message}";
}
}
IsLicensesOpen = true;
} }
private void SubmitBug() private void SubmitBug()
File diff suppressed because one or more lines are too long
+9 -1
View File
@@ -525,10 +525,18 @@ guardrails below — written down so a future "quick fix" never reintroduces a G
- **Use non-CC0 icon art** — the social bar's bundled SVG logo path data comes from **Simple Icons** - **Use non-CC0 icon art** — the social bar's bundled SVG logo path data comes from **Simple Icons**
(CC0 1.0, public domain — see `THIRD-PARTY-NOTICES.txt`). Replacing or adding logos must stay CC0 or (CC0 1.0, public domain — see `THIRD-PARTY-NOTICES.txt`). Replacing or adding logos must stay CC0 or
another public-domain source; a logo asset under a copyleft or attribution license would contaminate another public-domain source; a logo asset under a copyleft or attribution license would contaminate
the paid product. the paid product. The About hub's logo is the creator's own art ("llama fortnite superman logo",
`Assets/llama-logo.png`) — owned by us, so it carries no third-party license either.
- **Forget the v1 license-texts gate**`THIRD-PARTY-NOTICES.txt` links the canonical license texts; at - **Forget the v1 license-texts gate**`THIRD-PARTY-NOTICES.txt` links the canonical license texts; at
**v1 (GA)** the full texts of every license it names MUST ship alongside it (TASK 4 requirement 9 is the **v1 (GA)** the full texts of every license it names MUST ship alongside it (TASK 4 requirement 9 is the
release blocker). Queued early is wrong; the release pass owns it. release blocker). Queued early is wrong; the release pass owns it.
- **Break the in-app About requirement** — since TASK 21 the About overlay is the app's in-app creator
hub: the real logo, the creator links (YouTube channel, Mastodon, Buy me a coffee, and the greyed
"Unlock Premium" seam whose itch.io URL is tabled), and a **Licenses & legal** sub-panel that loads
`THIRD-PARTY-NOTICES.txt` from the executable directory and renders it in a scrolling panel —
**never the OS viewer, no Notepad, no browser tab**. The premium/coffee link constants live in
`MainViewModel` (`PremiumUrl`, `CoffeeUrl`, `ChannelUrl`, `MastodonUrl`); filling in the itch.io URL
lights up the premium button. Do not reintroduce an "open the notices in Notepad" button.
## Design Principle ## Design Principle
+70
View File
@@ -0,0 +1,70 @@
using System;
using Microsoft.Data.Sqlite;
using Xunit;
using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>
/// The About overlay is the in-app creator hub: it opens from the real window,
/// and its licensing sub-panel surfaces the full THIRD-PARTY-NOTICES.txt
/// in-app (scrolling text, never the OS viewer / Notepad). Drives the real VM
/// through the real window — same real-App + temp-DB pattern as the round-clip
/// and source-naming tests.
/// </summary>
[Collection("RealApp")]
public sealed class AboutHubTests
{
private readonly RealAppHost _app;
public AboutHubTests(RealAppHost app)
{
_app = app;
}
[Fact]
public void About_Opens_InApp_Licensing_Loads_Shipped_Notices()
{
_app.Run(Run);
}
private void Run()
{
var tempDb = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"ytLlive-about-{Guid.NewGuid():N}.db");
MainViewModel.LayoutPathOverride = tempDb;
var window = new MainWindow();
var vm = (MainViewModel)window.DataContext;
try
{
// About hub opens (the wordmark / gear menu entry point).
vm.OpenAboutCommand.Execute(null);
Assert.True(vm.IsAboutOpen);
Assert.False(vm.IsLicensesOpen);
// The hub links resolve to real destinations (channel + mastodon +
// coffee are live; premium is the tabled itch.io seam).
Assert.Equal("https://youtube.com/@llamachileshop", MainViewModel.ChannelUrl);
Assert.Equal("https://mastodon.llamachile.tube/@gramps", MainViewModel.MastodonUrl);
Assert.Equal("https://buymeacoffee.com/llamachiley", MainViewModel.CoffeeUrl);
// The licensing panel loads the full shipped notices text in-app.
vm.OpenLicensesCommand.Execute(null);
Assert.True(vm.IsLicensesOpen);
Assert.False(string.IsNullOrWhiteSpace(vm.LicensesText));
Assert.Contains("Third-Party Notices", vm.LicensesText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("LGPL", vm.LicensesText, StringComparison.OrdinalIgnoreCase);
// Back returns to the hub view (code-behind click handler path is
// equivalent; here we assert the VM contract the handler uses).
vm.IsLicensesOpen = false;
Assert.True(vm.IsAboutOpen);
}
finally
{
window.Close();
MainViewModel.LayoutPathOverride = null;
SqliteConnection.ClearAllPools();
try { System.IO.File.Delete(tempDb); } catch { /* best-effort cleanup */ }
}
}
}