Fix logo-click crash (Run is not a Visual); Text tab always visible; avatar initial fallback
This commit is contained in:
@@ -26,6 +26,8 @@
|
||||
- **`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.
|
||||
- **Dead code flagged, not touched:** `ShowWebcamCommand` / `CanShowWebcamInStagedScene` wired but unbound — audit item.
|
||||
|
||||
## Next steps
|
||||
|
||||
+10
-5
@@ -149,12 +149,18 @@
|
||||
</Style>
|
||||
</Ellipse.Style>
|
||||
</Ellipse>
|
||||
<!-- Avatar (connected only) -->
|
||||
<!-- Avatar (connected only); shows the channel initial when
|
||||
the profile image is missing or fails to download -->
|
||||
<Border Width="26" Height="26" CornerRadius="13" Background="#16213e" ClipToBounds="True"
|
||||
Margin="0,0,8,0" VerticalAlignment="Center"
|
||||
ToolTip="{Binding AccountDisplayName}"
|
||||
Visibility="{Binding IsConnected, Converter={StaticResource BoolToVis}}">
|
||||
<Image x:Name="AvatarImage" Stretch="UniformToFill"/>
|
||||
<Grid>
|
||||
<Image x:Name="AvatarImage" Stretch="UniformToFill"/>
|
||||
<TextBlock x:Name="AvatarInitial" Foreground="#e0e0e0" FontSize="13"
|
||||
FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Visibility="Collapsed"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<!-- Log In button (disconnected) -->
|
||||
<Button Content="Log In" Style="{StaticResource YtButton}"
|
||||
@@ -1114,12 +1120,11 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ═══ BROADCAST TEXT PULL-OUT (Live screen only) ═══
|
||||
<!-- ═══ BROADCAST TEXT PULL-OUT (always visible) ═══
|
||||
White "Text" tab on the preview's right edge; clicking it
|
||||
slides out the broadcast-metadata form. Edits persist
|
||||
locally immediately; Update pushes liveBroadcasts.update. -->
|
||||
<Grid Grid.Row="0"
|
||||
Visibility="{Binding IsBroadcastMetadataVisible, Converter={StaticResource BoolToVis}}">
|
||||
<Grid Grid.Row="0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Stretch">
|
||||
<Border Background="#16213e" ClipToBounds="True">
|
||||
<Border.Style>
|
||||
|
||||
+20
-6
@@ -135,24 +135,35 @@ public partial class MainWindow : Window
|
||||
|
||||
private void LoadAvatarImage()
|
||||
{
|
||||
if (_viewModel.IsConnected && !string.IsNullOrWhiteSpace(_viewModel.AccountAvatarUrl))
|
||||
var url = _viewModel.AccountAvatarUrl;
|
||||
if (_viewModel.IsConnected && !string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
try
|
||||
{
|
||||
var bitmap = new BitmapImage();
|
||||
bitmap.BeginInit();
|
||||
bitmap.UriSource = new Uri(_viewModel.AccountAvatarUrl, UriKind.Absolute);
|
||||
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}");
|
||||
}
|
||||
catch { AvatarImage.Source = null; }
|
||||
}
|
||||
else
|
||||
else if (_viewModel.IsConnected)
|
||||
{
|
||||
AvatarImage.Source = null;
|
||||
AppLog.Write("Avatar: connected but no profile image URL on the session");
|
||||
}
|
||||
AvatarImage.Source = null;
|
||||
AvatarInitial.Text = string.IsNullOrWhiteSpace(_viewModel.AccountDisplayName)
|
||||
? "?"
|
||||
: _viewModel.AccountDisplayName.Substring(0, 1).ToUpperInvariant();
|
||||
AvatarInitial.Visibility = _viewModel.IsConnected ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void SourceNameBox_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
@@ -311,7 +322,10 @@ public partial class MainWindow : Window
|
||||
private static bool IsDescendantOf(DependencyObject? child, DependencyObject ancestor)
|
||||
{
|
||||
while (child != null && !ReferenceEquals(child, ancestor))
|
||||
child = VisualTreeHelper.GetParent(child);
|
||||
{
|
||||
var parent = VisualTreeHelper.GetParent(child);
|
||||
child = parent ?? LogicalTreeHelper.GetParent(child);
|
||||
}
|
||||
return child != null;
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ Lifecycle: `created → ready → [testing] → live → complete` (transitional
|
||||
22. ✅ **Voice filters on the mic channel (2026-08-13 queued → SHIPPED 2026-08-14 inside TASK 9, the audio milestone)** — 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). Noise suppression = a **pure-C# noise gate** (creator chose over RNNoise / a second ffmpeg `afftdn` pipe, 2026-08-14 — KISS). Always-on — no UI knobs; the mic stays the creator's single audio control
|
||||
27. ✅ **Webcam row gates on the app-wide identity (2026-08-24, TASK 26)** — the (+) menu's Webcam item greys out whenever a webcam exists **anywhere** (`CanAddWebcam` = `StagedScene != null && _webcam == null`; renamed from `CanAddWebcamToStagedScene` whose per-scene rule let a second picker run from a scene lacking the config), raised at both `_webcam` mutation sites (create / last-config removal) + scene staging + elements change. Creator's visual pass found it: minis/no-capture-rows/capture-controls all good. ONE integration test `WebcamMenuGateTests.CanAddWebcam_Gates_On_The_AppWide_Webcam_Identity` (real window + temp DB seeded with a webcam in Starting; asserts greyed while Live staged, re-enabled after `RemoveSourceCommand` clears the last config + the `Webcam` DB row). Stale map fixed in the same commit: ai.md's "empty-canvas right-click Show Webcam" claim dropped — that XAML never shipped (`ShowWebcamCommand`/`CanShowWebcamInStagedScene` are wired but unbound dead code, audit item). 224 tests (223 pass; the pre-existing AudioPipelineTests failure is unrelated)
|
||||
28. ✅ **YouTube Chat layer: one-per-layout gate + legacy label heal (2026-08-24, TASK 27)** — same rule as TASK 26 applied to the chat layer: the (+) menu's YouTube Chat item greys out while any scene carries a ChatBox source (`CanAddYouTubeChat`, raised on staging + elements change; `AddSource` refuses a second), tooltip "One chat layer at a time — it's already in your stream". Plus the creator's label fix: layers added by commit `65641d8` were named "Chat"; LoadLayout now heals exactly that un-renamed default to "YouTube Chat" so the Layers row matches the (+) picklist (creator renames untouched, idempotent). ONE integration test `ChatLayerGateTests.LegacyChatName_Heals_And_CanAddYouTubeChat_Gates_On_The_Existing_Layer` (real window + temp DB with a legacy "Chat" row: healed on load + persisted on save, greyed cross-scene and in-scene, re-enabled after remove). 225 tests (224 pass; pre-existing AudioPipelineTests failure unrelated)
|
||||
29. ✅ **Broadcast metadata pull-out + launch geometry (2026-08-24)** — creator ask: "a tab-pullout on the right side of the preview pane — white tab, YouTube-red label reading 'Text' — opening a form with all settable liveBroadcast fields; fields that can't be filled before launch greyed out; saving/updating remote content; field data saved and pre-loaded on app run". Shipped as specified with one correction: the API is the inverse of "more fields once live" for contentDetails (those lock in created/ready) — the pull-out carries the always-editable snippet/status set: Title, Description, Tags (csv), Visibility (private/unlisted/public), Made-for-Kids, plus read-only Scheduled Start. **Live screen only** (`IsBroadcastMetadataVisible => IsLive`). White 30px tab, rotated red "Text", right edge vertically centered; click slides a dark 320px drawer left over the preview (200ms CubicEase). Every edit persists to Settings keys (`Broadcast.*`) immediately; **Update Broadcast** button calls the new `YouTubeStreamService.UpdateBroadcast` (PUT `liveBroadcasts?part=snippet,status`, echoes scheduledStartTime because update replaces the whole snippet part). Go Live prefills from the form and captures what was inserted (`CaptureGoLive`). The old un-persisted Default Stream Title/Description fields came OFF the App Settings overlay (replaced by the form); `DefaultStreamTitle/Description` VM properties deleted. **Launch geometry** (same unit): default 1920×1040, MinWidth 1366, MinHeight 768; `WindowStartupLocation=Manual` + window size/position persisted on close via `RestoreBounds` (maximized-safe), restored in ctor clamped to minimums and the primary work area (disconnected-secondary fallback). ONE integration test `BroadcastPullOutTests.Metadata_Persists_WindowRestores_Clamped_And_UpdatePatchesRemote`. 228 tests (227 pass; pre-existing AudioPipelineTests failure unrelated)
|
||||
29. ✅ **Broadcast metadata pull-out + launch geometry (2026-08-24)** — creator ask: "a tab-pullout on the right side of the preview pane — white tab, YouTube-red label reading 'Text' — opening a form with all settable liveBroadcast fields; fields that can't be filled before launch greyed out; saving/updating remote content; field data saved and pre-loaded on app run". Shipped as specified with one correction: the API is the inverse of "more fields once live" for contentDetails (those lock in created/ready) — the pull-out carries the always-editable snippet/status set: Title, Description, Tags (csv), Visibility (private/unlisted/public), Made-for-Kids, plus read-only Scheduled Start. **Always visible** (creator revised same day from Live-only gating). White 30px tab, rotated red "Text", right edge vertically centered; click slides a dark 320px drawer left over the preview (200ms CubicEase). Every edit persists to Settings keys (`Broadcast.*`) immediately; **Update Broadcast** button calls the new `YouTubeStreamService.UpdateBroadcast` (PUT `liveBroadcasts?part=snippet,status`, echoes scheduledStartTime because update replaces the whole snippet part). Go Live prefills from the form and captures what was inserted (`CaptureGoLive`). The old un-persisted Default Stream Title/Description fields came OFF the App Settings overlay (replaced by the form); `DefaultStreamTitle/Description` VM properties deleted. **Launch geometry** (same unit): default 1920×1040, MinWidth 1366, MinHeight 768; `WindowStartupLocation=Manual` + window size/position persisted on close via `RestoreBounds` (maximized-safe), restored in ctor clamped to minimums and the primary work area (disconnected-secondary fallback). ONE integration test `BroadcastPullOutTests.Metadata_Persists_WindowRestores_Clamped_And_UpdatePatchesRemote`. 228 tests (227 pass; pre-existing AudioPipelineTests failure unrelated)
|
||||
|
||||
### The Minimal Source Set (design decision — do not expand casually)
|
||||
|
||||
|
||||
@@ -463,7 +463,6 @@ public class MainViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(IsOffline));
|
||||
OnPropertyChanged(nameof(IsLive));
|
||||
OnPropertyChanged(nameof(IsEditMode));
|
||||
OnPropertyChanged(nameof(IsBroadcastMetadataVisible));
|
||||
OnPropertyChanged(nameof(RecDotBrush));
|
||||
OnPropertyChanged(nameof(RecTextBrush));
|
||||
OnPropertyChanged(nameof(RecDotOpacity));
|
||||
@@ -1247,9 +1246,6 @@ public class MainViewModel : ViewModelBase
|
||||
/// succeeds. Gates the pull-out's Update button (nothing remote to patch).</summary>
|
||||
public string? CurrentBroadcastId => _currentBroadcastId;
|
||||
|
||||
/// <summary>The pull-out exists on the Live screen only — visible while streaming.</summary>
|
||||
public bool IsBroadcastMetadataVisible => IsLive;
|
||||
|
||||
public Services.LiveBroadcastFormViewModel BroadcastForm { get; }
|
||||
|
||||
// Window geometry pass-through (code-behind owns the WPF specifics).
|
||||
|
||||
@@ -195,7 +195,7 @@ The app is a **director's control surface**, not an editor. This replaces the OB
|
||||
|
||||
### Broadcast metadata pull-out + launch geometry (2026-08-24)
|
||||
|
||||
**The "Text" tab** (Live screen only): a white 30px tab with rotated YouTube-red "Text" sits on the right edge of the preview, vertically centered, visible only while `IsLive` (`IsBroadcastMetadataVisible`). Click slides out a 320px dark drawer over the preview's right edge (200ms CubicEase on `Width`, `BroadcastForm.IsDrawerOpen` drives it; ✕ closes).
|
||||
**The "Text" tab** (always visible, 2026-08-24 revision): a white 30px tab with rotated YouTube-red "Text" sits on the right edge of the preview, vertically centered — **shown regardless of login/stream state** (creator decision; original IsLive gating removed same day). Click slides out a 320px dark drawer over the preview's right edge (200ms CubicEase on `Width`, `BroadcastForm.IsDrawerOpen` drives it; ✕ closes). The Update button still greys until `CurrentBroadcastId` exists.
|
||||
|
||||
**Fields — the always-editable snippet/status set** (⚠️ creator premise inverted: contentDetails fields like latency/DVR/embed are editable ONLY in created/ready, NOT while live; they are deliberately absent):
|
||||
- Title / Description / Tags (comma-separated string → `snippet.tags[]`) / Visibility ComboBox (private/unlisted/public) / Made-for-Kids CheckBox — all live-editable pre- and post-launch
|
||||
|
||||
@@ -76,10 +76,11 @@ public sealed class BroadcastPullOutTests
|
||||
vm.BroadcastForm.Load();
|
||||
Assert.Equal("Hello", vm.BroadcastForm.Title);
|
||||
|
||||
vm.StreamStatus = StreamStatus.Streaming;
|
||||
Assert.True(vm.IsBroadcastMetadataVisible);
|
||||
vm.StreamStatus = StreamStatus.Offline;
|
||||
Assert.False(vm.IsBroadcastMetadataVisible);
|
||||
Assert.False(vm.BroadcastForm.IsDrawerOpen);
|
||||
vm.BroadcastForm.OpenDrawerCommand.Execute(null);
|
||||
Assert.True(vm.BroadcastForm.IsDrawerOpen);
|
||||
vm.BroadcastForm.CloseDrawerCommand.Execute(null);
|
||||
Assert.False(vm.BroadcastForm.IsDrawerOpen);
|
||||
|
||||
using (var writer = new LayoutStore(tempDb))
|
||||
writer.SaveWindowState(100, 50, 1500, 900, "Normal");
|
||||
|
||||
Reference in New Issue
Block a user