From 100980087efe5b6514ae6c0db58a0611c6e8b828 Mon Sep 17 00:00:00 2001 From: gramps Date: Mon, 24 Aug 2026 14:57:34 -0700 Subject: [PATCH] Fix Run-crash for real (type-guard before VisualTreeHelper); avatar via HttpClient decode --- HANDOFF.md | 14 ++++++---- MainWindow.xaml.cs | 70 +++++++++++++++++++++++++++++----------------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index fc3330b..badbaa8 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -26,15 +26,17 @@ - **`Source.Type` setter derives `IsBackground`** (Models/Source.cs:40) — any Type change must re-set IsBackground after. - **vstest quirk:** only one `--tests:` param allowed; plain substrings work, alternation regexes silently match nothing. - Notification.Wpf gotchas (ai.md/TASKS.md): area routing by XAML `Name`; unknown AreaName silently drops; `NeverExpires()` = `TimeSpan.MaxValue`; `ToHex()` = `#AARRGGBB`. -- **Text-tab crash fix (2026-08-24):** clicking any text `Run` (e.g. the LlamaCasty logo) crashed the app — `Window_PreviewMouseLeftButtonDown → IsDescendantOf` used only `VisualTreeHelper.GetParent`, which throws on non-Visuals; now falls back to `LogicalTreeHelper.GetParent`. Landmine for ANY new hit-testing code: text Runs are not Visuals. -- **Avatar display:** connected-but-image-missing now shows the channel initial in the circle + logs to startup.log (`Avatar load failed` / `connected but no profile image URL`). If the creator reports "no avatar", first check whether the app is actually CONNECTED (green status light vs Log In button) — ending a stream clears the OAuth session by design. +- **Text-Run crash (2026-08-24, fixed twice — first fix was WRONG):** clicking any text `Run` (e.g. the LlamaCasty logo) crashed the app in `Window_PreviewMouseLeftButtonDown → IsDescendantOf`. First attempt added `parent ?? LogicalTreeHelper.GetParent(child)` — useless, because `VisualTreeHelper.GetParent(Run)` **throws** before `??` ever evaluates. Real fix: type-guard `child is Visual or Visual3D` BEFORE choosing the helper. Landmine for ANY new hit-testing code: text Runs are not Visuals. +- **Avatar images: never decode via BitmapImage.UriSource on this box** — WPF's internal downloader fails here with "This Freezable cannot be frozen" (startup.log 2026-08-24). Pattern that works: `HttpClient.GetByteArrayAsync(url)` → `MemoryStream` → `StreamSource` + `OnLoad` → `Freeze()` (`MainWindow.LoadAvatarImage`). Initial-letter fallback remains for empty URL/failed download; failures log to startup.log. Ending a stream still signs out by design. +- **USER RULE: never `git push` without explicit instruction.** Commit locally per work unit; push only when asked. - **Dead code flagged, not touched:** `ShowWebcamCommand` / `CanShowWebcamInStagedScene` wired but unbound — audit item. ## Next steps -1. Creator visual pass: Text tab appears only while streaming; drawer slides over preview; fields pre-load across restarts; Update button greys until Go Live succeeds (needs real OAuth round-trip to verify remotely). -2. First-launch check: window opens 1920×1040 centered; resize/move/close/reopen restores geometry; can't shrink below 1366×768. -3. Continue queue: TASK 20 step 2 (hotkey config UI + persistence), TASK 17 web source, TASK 18 local recording, TASK 21 media source. -4. Loose ends: dead `ShowWebcamCommand`, AudioPipelineTests failure, Velopack update URL. +1. **Queued: wire up "the event button"** — creator asked for code behind it; which control exactly is still to be confirmed (no control in any XAML is named/labeled "event"). Do not guess. +2. Creator visual pass: Text tab always visible, tab toggles drawer, click-away collapses it; fields pre-load across restarts; Update button greys until Go Live succeeds (needs real OAuth round-trip to verify remotely). +3. Verify on next launch: LlamaCasty logo click opens About without crashing; avatar shows the real YouTube profile image (not the initial) — startup.log should have no `Avatar load failed` line. +4. Continue queue: TASK 20 step 2 (hotkey config UI + persistence), TASK 17 web source, TASK 18 local recording, TASK 21 media source. +5. Loose ends: dead `ShowWebcamCommand`, AudioPipelineTests failure, Velopack update URL. ## Build ```bash diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 70eedd8..5372989 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -1,5 +1,8 @@ -using System.Collections; +using System; +using System.Collections; using System.ComponentModel; +using System.IO; +using System.Net.Http; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -133,37 +136,51 @@ public partial class MainWindow : Window : null; } - private void LoadAvatarImage() + private static readonly HttpClient AvatarHttp = new() { Timeout = TimeSpan.FromSeconds(15) }; + private string? _loadedAvatarUrl; + + private async void LoadAvatarImage() { var url = _viewModel.AccountAvatarUrl; - if (_viewModel.IsConnected && !string.IsNullOrWhiteSpace(url)) + if (!_viewModel.IsConnected) { - try - { - var bitmap = new BitmapImage(); - bitmap.BeginInit(); - bitmap.UriSource = new Uri(url, UriKind.Absolute); - bitmap.CacheOption = BitmapCacheOption.OnLoad; - bitmap.EndInit(); - bitmap.Freeze(); - AvatarImage.Source = bitmap; - AvatarInitial.Visibility = Visibility.Collapsed; - return; - } - catch (Exception ex) - { - AppLog.Write($"Avatar load failed ({url}): {ex.Message}"); - } + ShowAvatarFallback(null); + return; } - else if (_viewModel.IsConnected) + if (string.IsNullOrWhiteSpace(url)) { AppLog.Write("Avatar: connected but no profile image URL on the session"); + ShowAvatarFallback(null); + return; } + if (url == _loadedAvatarUrl && AvatarImage.Source != null) return; + try + { + var bytes = await AvatarHttp.GetByteArrayAsync(url); + using var ms = new MemoryStream(bytes); + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.StreamSource = ms; + bitmap.EndInit(); + bitmap.Freeze(); + _loadedAvatarUrl = url; + AvatarImage.Source = bitmap; + AvatarInitial.Visibility = Visibility.Collapsed; + } + catch (Exception ex) + { + AppLog.Write($"Avatar load failed ({url}): {ex.Message}"); + ShowAvatarFallback(_viewModel.AccountDisplayName); + } + } + + private void ShowAvatarFallback(string? name) + { + _loadedAvatarUrl = null; AvatarImage.Source = null; - AvatarInitial.Text = string.IsNullOrWhiteSpace(_viewModel.AccountDisplayName) - ? "?" - : _viewModel.AccountDisplayName.Substring(0, 1).ToUpperInvariant(); - AvatarInitial.Visibility = _viewModel.IsConnected ? Visibility.Visible : Visibility.Collapsed; + AvatarInitial.Text = string.IsNullOrWhiteSpace(name) ? "?" : name.Substring(0, 1).ToUpperInvariant(); + AvatarInitial.Visibility = Visibility.Visible; } private void SourceNameBox_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) @@ -326,8 +343,9 @@ public partial class MainWindow : Window { while (child != null && !ReferenceEquals(child, ancestor)) { - var parent = VisualTreeHelper.GetParent(child); - child = parent ?? LogicalTreeHelper.GetParent(child); + child = child is System.Windows.Media.Visual or System.Windows.Media.Media3D.Visual3D + ? VisualTreeHelper.GetParent(child) + : LogicalTreeHelper.GetParent(child); } return child != null; }