feat(about): rebuild the About box as a conversion surface
Creator: the About box was "shitty" — an unreadable logo, a version number nobody asked for, copy that insulted the creator, and buttons that looked like text. Rebuilt around one job: convert. - **820×860 modal** (was 650×700) with a scrollable hub, and the logo at **520 px wide** instead of 125 — the asset is 2816×1536, so it was being drawn as a thumbnail, not an icon problem. - **Pre-Release pill** replaces "Version 1.0.0" (the assembly version stays in log lines). The pill is a button: it opens llamacasty.com until a Steam store page exists, so the link changes in one place. - Tagline: "1-2-3, and go live to the world on YouTube!" The old "simple enough that even the most right-brained person can intuit it" is gone, and a test asserts it stays gone. - The offer in three short blocks (free 30 fps · paid 60 fps · one-time, lifetime, no account / subscription / tracking), then the launch price and **one primary CTA**: "Unlock 60 fps — $79". - Price: $79 until **April 1, 2027**, then $99 locked for life. The numbers live once, in `MainViewModel.Overlays`, and are recorded in `docs/ai/commercial.md`. - **A paying customer is never sold to**: ownership collapses the price/CTA block and shows "Licence: owned via Steam — thank you." - Secondary destinations (channel, Mastodon, coffee, licences) are one quiet row; footer is `© Llama Chile Shop`; the "Made by gramps" line is removed. - `YtButtonSecondary` now carries a higher surface, a hairline border and a keyboard focus ring — they were the same tone as the panel behind them, which is why they read as plain text. Test: `AboutHubTests.About_ShowsTheOfferToFreeUsers_AndNothingToSellToOwners` — tagline, pill, price, CTA text, copyright, the removed lines stay removed, and the CTA block collapses when owned. FAST gate: 540 passed, 0 failed, 0 warnings.
This commit is contained in:
+84
-35
@@ -15,7 +15,7 @@
|
||||
</Button.Template>
|
||||
</Button>
|
||||
|
||||
<Border Background="{StaticResource Brush.Background}" CornerRadius="8" Width="650" MaxHeight="700"
|
||||
<Border Background="{StaticResource Brush.Background}" CornerRadius="8" Width="820" MaxHeight="860"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||
Padding="28" BorderBrush="#333" BorderThickness="1">
|
||||
<Grid>
|
||||
@@ -76,42 +76,91 @@
|
||||
Background="{StaticResource Brush.SurfaceContainerLow}"
|
||||
Visibility="{Binding IsAboutOpen, Converter={StaticResource BoolToVis}}">
|
||||
|
||||
<!-- Hub view -->
|
||||
<StackPanel Visibility="{Binding IsHubVisible, Converter={StaticResource BoolToVis}}">
|
||||
<Image Source="/LlamaCasty;component/Assets/llamacasty-logo.jpg" Height="125" Stretch="Uniform"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,12"/>
|
||||
<TextBlock Text="{Binding AppVersionLabel}" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="12"
|
||||
HorizontalAlignment="Center" Margin="0,4,0,0"/>
|
||||
<TextBlock Text="Live streaming for YouTube — simple enough that even the most right-brained person can intuit it."
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="13" TextWrapping="Wrap" MaxWidth="380"
|
||||
TextAlignment="Center" Margin="0,16,0,0"/>
|
||||
<TextBlock x:Name="UpgradeCopy"
|
||||
Text="Free at 720p30. The paid version unlocks higher stream and recording quality. One-time purchase; no subscription. Launch price $79, then $99 on the announced date."
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="12"
|
||||
TextWrapping="Wrap" MaxWidth="380" TextAlignment="Center" Margin="0,12,0,0"/>
|
||||
<Button x:Name="UpgradeLinkButton" Content="Learn more at llamacasty.com"
|
||||
Command="{Binding OpenPremiumCommand}" Style="{StaticResource YtButtonSecondary}"
|
||||
HorizontalAlignment="Center" Margin="0,12,0,0"
|
||||
ToolTip="Opens llamacasty.com in your browser when clicked"/>
|
||||
<TextBlock Text="Made by gramps · llama chile shop" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="12"
|
||||
HorizontalAlignment="Center" Margin="0,16,0,8"/>
|
||||
<!-- Hub view — a conversion surface, not a credits page (creator, 2026-11-05):
|
||||
large logo, one tagline, the offer in three short blocks, one primary CTA,
|
||||
everything else demoted. A paying customer never sees the price. -->
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
Visibility="{Binding IsHubVisible, Converter={StaticResource BoolToVis}}">
|
||||
<StackPanel>
|
||||
<Image Source="/LlamaCasty;component/Assets/llamacasty-logo.jpg"
|
||||
Width="520" Stretch="Uniform" HorizontalAlignment="Center" Margin="0,0,0,10"/>
|
||||
|
||||
<!-- 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 Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Button Content="Buy me a coffee" Command="{Binding OpenCoffeeCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}"/>
|
||||
</StackPanel>
|
||||
<Button x:Name="ReleaseBadgeButton" Content="{Binding AppReleaseLabel}"
|
||||
Style="{StaticResource PillBadgeButton}"
|
||||
Command="{Binding OpenPremiumCommand}"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,10"
|
||||
ToolTip="Pre-release — opens llamacasty.com when clicked"/>
|
||||
|
||||
<Button Content="Licenses & legal" Command="{Binding OpenLicensesCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}" HorizontalAlignment="Center"
|
||||
Margin="0,16,0,0"/>
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="AboutTagline"
|
||||
Text="1-2-3, and go live to the world on YouTube!"
|
||||
Foreground="{StaticResource Brush.OnSurface}" FontSize="17" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" MaxWidth="640" TextAlignment="Center"
|
||||
HorizontalAlignment="Center" Margin="0,4,0,18"/>
|
||||
|
||||
<Grid MaxWidth="640" HorizontalAlignment="Center" Margin="0,0,0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Free" Margin="0,0,14,4"
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="13" FontWeight="SemiBold"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding FreeOfferText}" Margin="0,0,0,4"
|
||||
Foreground="{StaticResource Brush.OnSurface}" FontSize="13" TextWrapping="Wrap"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Paid" Margin="0,0,14,4"
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="13" FontWeight="SemiBold"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding PaidOfferText}" Margin="0,0,0,4"
|
||||
Foreground="{StaticResource Brush.OnSurface}" FontSize="13" TextWrapping="Wrap"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.ColumnSpan="2" Text="{Binding TrustLine}"
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="12"
|
||||
TextWrapping="Wrap" Margin="0,6,0,0"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Free: the offer. Owned: thanks, no price. -->
|
||||
<StackPanel x:Name="UpgradeBlock"
|
||||
Visibility="{Binding IsAppOwned, Converter={StaticResource InverseBoolToVis}}"
|
||||
HorizontalAlignment="Center" Margin="0,18,0,0">
|
||||
<TextBlock x:Name="AboutPriceLine" Text="{Binding PriceLine}"
|
||||
Foreground="{StaticResource Brush.OnSurface}" FontSize="16" FontWeight="Bold"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding PriceNote}" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="12"
|
||||
TextAlignment="Center" TextWrapping="Wrap" MaxWidth="520" Margin="0,2,0,10"/>
|
||||
<Button x:Name="UpgradeCtaButton" Content="{Binding UpgradeCtaLabel}"
|
||||
Command="{Binding OpenPremiumCommand}"
|
||||
Style="{StaticResource YtButton}"
|
||||
HorizontalAlignment="Center" Padding="22,10" FontSize="14"
|
||||
ToolTip="Opens llamacasty.com in your browser when clicked"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock x:Name="OwnedStateText" Text="{Binding LicenceStateLabel}"
|
||||
Foreground="{StaticResource Brush.Positive}" FontSize="13" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" Margin="0,18,0,0"
|
||||
Visibility="{Binding IsAppOwned, Converter={StaticResource BoolToVis}}"/>
|
||||
|
||||
<!-- Secondary destinations, one row, deliberately quieter than the CTA. -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,22,0,0">
|
||||
<Button Content="llama chile shop on YouTube" Command="{Binding OpenChannelCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0" FontSize="11" Padding="10,5"/>
|
||||
<Button Content="Mastodon" Command="{Binding OpenMastodonCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0" FontSize="11" Padding="10,5"/>
|
||||
<Button Content="Buy me a coffee" Command="{Binding OpenCoffeeCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0" FontSize="11" Padding="10,5"/>
|
||||
<Button Content="Licenses & legal" Command="{Binding OpenLicensesCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}" FontSize="11" Padding="10,5"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock x:Name="AboutCopyright" Text="© Llama Chile Shop"
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11"
|
||||
HorizontalAlignment="Center" Margin="0,18,0,4"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Licensing/legal panel -->
|
||||
<StackPanel Visibility="{Binding IsLicensesOpen, Converter={StaticResource BoolToVis}}">
|
||||
|
||||
+58
-1
@@ -48,8 +48,65 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Secondary buttons used to be YtButton with the SAME tone as the panel behind them, so they
|
||||
read as plain text (creator, 2026-11-05: "you can tell they're buttons b/c our styling hasn't
|
||||
been applied"). They now sit on a higher surface with a hairline border, and show a focus ring
|
||||
so a keyboard user can find the one that matters. -->
|
||||
<Style x:Key="YtButtonSecondary" TargetType="Button" BasedOn="{StaticResource YtButton}">
|
||||
<Setter Property="Background" Value="{StaticResource Brush.SurfaceContainer}"/>
|
||||
<Setter Property="Background" Value="{StaticResource Brush.SurfaceContainerHigh}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource Brush.OnSurface}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Grid>
|
||||
<Border x:Name="Bd" Background="{TemplateBinding Background}"
|
||||
BorderBrush="{StaticResource Brush.Outline}" BorderThickness="1"
|
||||
CornerRadius="{StaticResource Radius.Sm}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="Glow" Background="White" CornerRadius="{StaticResource Radius.Sm}" Opacity="0"/>
|
||||
<Border x:Name="Focus" BorderBrush="{StaticResource Brush.Primary}" BorderThickness="2"
|
||||
CornerRadius="{StaticResource Radius.Sm}" Opacity="0" Margin="-2"/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Glow" Property="Opacity" Value="0.08"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Glow" Property="Opacity" Value="0.16"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||
<Setter TargetName="Focus" Property="Opacity" Value="1"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.45"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- The release badge: a pill that is itself a button (it links to the product site until the
|
||||
Steam store page exists). -->
|
||||
<Style x:Key="PillBadgeButton" TargetType="Button">
|
||||
<Setter Property="Foreground" Value="{StaticResource Brush.OnSurfaceVariant}"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Padding" Value="10,3"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{StaticResource Brush.SurfaceContainerHigh}"
|
||||
BorderBrush="{StaticResource Brush.Outline}" BorderThickness="1"
|
||||
CornerRadius="999" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="IconButton" TargetType="Button">
|
||||
|
||||
@@ -25,6 +25,7 @@ public partial class MainViewModel
|
||||
EntitlementErrorMessage = _entitlement.ErrorMessage;
|
||||
if (!changed) return;
|
||||
OnPropertyChanged(nameof(HasProOutput));
|
||||
OnPropertyChanged(nameof(LicenceStateLabel));
|
||||
RefreshSteamLicenceAlert();
|
||||
if (!HasProOutput && _selectedQuality is { RequiresPro: true } && CanChangeQuality)
|
||||
SelectedQuality = QualityOptions.Single(option => option.Display == OutputProfile.FreeFallbackName);
|
||||
@@ -78,6 +79,7 @@ public partial class MainViewModel
|
||||
if (!DevBuild.IsDevelopment) return;
|
||||
if (!SetProperty(ref _developerMode, value)) return;
|
||||
OnPropertyChanged(nameof(HasProOutput));
|
||||
OnPropertyChanged(nameof(LicenceStateLabel));
|
||||
RefreshSteamLicenceAlert();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,27 @@ public partial class MainViewModel : ViewModelBase
|
||||
public const string CoffeeUrl = "https://buymeacoffee.com/llamachiley";
|
||||
public static string AppVersionLabel => $"Version {typeof(MainViewModel).Assembly.GetName().Version?.ToString(3)}";
|
||||
|
||||
// ── About box copy (creator ruling, 2026-11-05). The price and its end date live HERE, once, so
|
||||
// the app, the site and docs/commercial.md cannot drift apart. ──
|
||||
public const string LaunchPrice = "$79";
|
||||
public const string StandardPrice = "$99";
|
||||
public const string LaunchPriceEndsLabel = "April 1, 2027";
|
||||
|
||||
/// <summary>The release state shown as a badge. Not the assembly version — that number is for
|
||||
/// log lines, not for the creator.</summary>
|
||||
public static string AppReleaseLabel => "Pre-Release";
|
||||
|
||||
public string UpgradeCtaLabel => $"Unlock 60 fps — {LaunchPrice}";
|
||||
public string PriceLine => $"Launch price {LaunchPrice}";
|
||||
public string PriceNote => $"Until {LaunchPriceEndsLabel} — after that {StandardPrice}, locked for life.";
|
||||
public string FreeOfferText => "Livestream and record at 30 fps.";
|
||||
public string PaidOfferText => "Unlocks 60 fps for streaming and recording.";
|
||||
public string TrustLine => "One-time purchase · lifetime licence · no account, no subscription, no tracking.";
|
||||
|
||||
/// <summary>Shown instead of the price and CTA once ownership is verified: a paying customer is
|
||||
/// never sold to.</summary>
|
||||
public string LicenceStateLabel => IsAppOwned ? "Licence: owned via Steam — thank you." : string.Empty;
|
||||
|
||||
/// <summary>The About hub's optional product-information link.</summary>
|
||||
public bool IsPremiumAvailable => PremiumUrl.Length > 0;
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
## Price (creator ruling 2026-11-05)
|
||||
|
||||
- **Launch price $79** — one-time purchase, lifetime licence, no subscription, no account, no tracking.
|
||||
- **Promotion ends April 1, 2027**; after that the price is **$99, locked for life**.
|
||||
- Free tier: livestream and record at **30 fps**. Paid unlocks **60 fps** for both.
|
||||
- The numbers live in `ViewModels/MainViewModel.Overlays.cs` (`LaunchPrice`, `StandardPrice`,
|
||||
`LaunchPriceEndsLabel`) so the About box, the site copy and this file cannot drift apart. Change them
|
||||
there first, then here.
|
||||
|
||||
# Commercial: pricing, entitlement, channels, support
|
||||
|
||||
The single source of truth for what LlamaCasty charges, what a licence entitles, how it ships and
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
## About box (2026-11-05)
|
||||
|
||||
A conversion surface, not a credits page: **820×860 modal**, ~520 px logo, a `Pre-Release` pill that is
|
||||
itself a button (links to llamacasty.com until a Steam store page exists), the tagline
|
||||
"1-2-3, and go live to the world on YouTube!", the offer as three short blocks (free 30 fps · paid
|
||||
60 fps · one-time/lifetime/no-tracking), then the launch price and **one primary CTA**
|
||||
("Unlock 60 fps — $79"). Secondary destinations are one quiet row; the footer is `© Llama Chile Shop`.
|
||||
|
||||
**A paying customer is never sold to:** with ownership verified the price/CTA block collapses and is
|
||||
replaced by `Licence: owned via Steam — thank you.` The old tagline ("simple enough that even the most
|
||||
right-brained person can intuit it") and the "Made by gramps" line are gone for good.
|
||||
`YtButtonSecondary` carries a higher surface, a hairline border and a keyboard focus ring — before this
|
||||
the secondary buttons were the same tone as the panel behind them and read as plain text.
|
||||
|
||||
## Left panel alert stack (2026-11-05)
|
||||
|
||||
A stack of red rows at the top of the left panel, **above both mode grids** (so it shows while editing
|
||||
|
||||
@@ -75,6 +75,69 @@ public sealed class AboutHubTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The About box is a conversion surface (creator, 2026-11-05): a large logo, the 1-2-3 tagline,
|
||||
/// the offer, one primary CTA with the price on it — and a paying customer sees none of the price.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void About_ShowsTheOfferToFreeUsers_AndNothingToSellToOwners()
|
||||
{
|
||||
_app.Run(() =>
|
||||
{
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"llamacasty-about-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
MainWindow? window = null;
|
||||
try
|
||||
{
|
||||
window = new MainWindow();
|
||||
var vm = (MainViewModel)window.DataContext;
|
||||
vm.EntitlementOverride = new FakeEntitlement(false, "Steam licensing is not configured in this build.");
|
||||
window.Show();
|
||||
vm.OpenAboutCommand.Execute(null);
|
||||
window.UpdateLayout();
|
||||
|
||||
var badge = FindVisualChildByName<Button>(window, "ReleaseBadgeButton");
|
||||
Assert.NotNull(badge);
|
||||
Assert.Equal("Pre-Release", badge!.Content);
|
||||
Assert.NotNull(badge.Command); // the badge links out until the Steam page exists
|
||||
|
||||
var tagline = FindVisualChildByName<TextBlock>(window, "AboutTagline");
|
||||
Assert.NotNull(tagline);
|
||||
Assert.Equal("1-2-3, and go live to the world on YouTube!", tagline!.Text);
|
||||
// The old line insulted the creator and is gone for good.
|
||||
Assert.DoesNotContain("right-brained", TextOf(window));
|
||||
|
||||
var price = FindVisualChildByName<TextBlock>(window, "AboutPriceLine");
|
||||
Assert.Equal("Launch price $79", price!.Text);
|
||||
|
||||
var cta = FindVisualChildByName<Button>(window, "UpgradeCtaButton");
|
||||
Assert.NotNull(cta);
|
||||
Assert.Equal("Unlock 60 fps — $79", cta!.Content);
|
||||
var upgradeBlock = FindVisualChildByName<StackPanel>(window, "UpgradeBlock");
|
||||
Assert.Equal(Visibility.Visible, upgradeBlock!.Visibility);
|
||||
|
||||
var copyright = FindVisualChildByName<TextBlock>(window, "AboutCopyright");
|
||||
Assert.Equal("© Llama Chile Shop", copyright!.Text);
|
||||
Assert.DoesNotContain("Made by gramps", TextOf(window));
|
||||
|
||||
// A paying customer is never sold to.
|
||||
vm.EntitlementOverride = new FakeEntitlement(true);
|
||||
window.UpdateLayout();
|
||||
Assert.Equal(Visibility.Collapsed, FindVisualChildByName<StackPanel>(window, "UpgradeBlock")!.Visibility);
|
||||
var owned = FindVisualChildByName<TextBlock>(window, "OwnedStateText");
|
||||
Assert.Equal(Visibility.Visible, owned!.Visibility);
|
||||
Assert.Contains("owned", owned.Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
window?.Close();
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Free_user_gets_1080p30_and_cannot_select_or_change_to_60fps()
|
||||
{
|
||||
@@ -154,6 +217,30 @@ public sealed class AboutHubTests
|
||||
});
|
||||
}
|
||||
|
||||
private static T? FindVisualChildByName<T>(DependencyObject parent, string name) where T : DependencyObject
|
||||
{
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is FrameworkElement element && element.Name == name && child is T typed) return typed;
|
||||
if (FindVisualChildByName<T>(child, name) is { } found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Every TextBlock string in the tree — for asserting that removed copy is really gone.</summary>
|
||||
private static string TextOf(DependencyObject parent)
|
||||
{
|
||||
var text = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is TextBlock block) text.Append(block.Text).Append('\n');
|
||||
text.Append(TextOf(child));
|
||||
}
|
||||
return text.ToString();
|
||||
}
|
||||
|
||||
private static T? FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
|
||||
{
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
|
||||
Reference in New Issue
Block a user