Files
gramps e69db4d26c feat(ui): TASK 42 top bar redesign — one Record-or-Stream surface (2026-09-22)
The bar now renders ONE surface from the FIRST decision the creator makes —
Record or Stream — with each world being the whole bar. Creator directive:
"I wrote the fucking thing and I still can't figure-out how to do stuff",
"forget this one dog plan bullshit". OBS/streaming-tool reference for the
one-surface paradigm: https://obsproject.com (single mode toggle + context
actions) — cited per spin-guard habit; the go-live/test pattern follows
CEV (https://cev-desktop.aSean.xyz) precedent.

- Mode: single segmented REC|ON-AIR switch (SegmentToggle/SegmentLabel in
  Themes/Controls.xaml); radio-exclusive world selection, one tap to flip.
- Record world: switch + Start Recording, zero YouTube identity.
- Stream world: switch + Go Live + Test + account zone right (Sign In until
  connected, then avatar with Change Account/Logout context menu).
- Test is a child of Stream: procs only stream-armed AND signed-in.
- Running: one reality line "dot word elapsed" (REC/LIVE/TEST; green/red/
  gold) + End; worlds + switch retire.
- Gear moved up from bottom bar, ~3 wordmark letters past the brand, single
  click opens Settings/Bug/Feature/About menu (TASK 40 Unit B shipped early).
- Fix folded in: BeginTestStream now arms OnAirPillOn explicitly (unarmed
  pump booted with zero encoder outputs -> "At least one output is required"
  forced-stop cascade from TASK 41's pipeline).
- VM: world/reality props + RaiseTopBarModes() wired into StreamStatus,
  pill, IsRecording, IsTestStream, IsConnected setters.

Doc: ai.md top-bar model, ViewModels/index.md, Controls/index.md, TASKS.md,
HANDOFF.md, new TASKS/task-42-top-bar-redesign.md; task-40 note.

Tests: TopBarModeTests.cs (new Good Dog), PillRadioTests + TestStreamTests
gates updated. Build 0 warnings; full suite 314/315 — single abort is the
pre-existing env-dependent RealMouseDrag test (Path of Exile 2 running)
2026-09-22 17:57:06 -07:00

85 lines
2.7 KiB
C#

using System;
using System.IO;
using System.Net.Http;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using ytLive.Helpers;
using ytLive.ViewModels;
namespace ytLive.Controls;
public partial class TopBar : UserControl
{
private static readonly HttpClient AvatarHttp = new() { Timeout = TimeSpan.FromSeconds(15) };
private string? _loadedAvatarUrl;
public TopBar()
{
InitializeComponent();
AvatarImage.Source = null;
AvatarInitial.Visibility = System.Windows.Visibility.Collapsed;
}
public void RefreshAvatar()
{
_ = LoadAvatarImageAsync();
}
private void GearButton_Click(object sender, RoutedEventArgs e)
{
// One menu (Settings / Bug / Feature / About), opened on click — moved up
// from the bottom bar so app-level actions live next to the brand.
if (sender is Button { ContextMenu: { } menu } button)
{
menu.PlacementTarget = button;
menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
menu.IsOpen = true;
}
}
private async System.Threading.Tasks.Task LoadAvatarImageAsync()
{
var vm = DataContext as MainViewModel;
var url = vm?.AccountAvatarUrl;
if (vm is not { IsConnected: true })
{
ShowAvatarFallback(null);
return;
}
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 = System.Windows.Visibility.Collapsed;
}
catch (Exception ex)
{
AppLog.Write($"Avatar load failed ({url}): {ex.Message}");
ShowAvatarFallback(vm?.AccountDisplayName);
}
}
private void ShowAvatarFallback(string? name)
{
_loadedAvatarUrl = null;
AvatarImage.Source = null;
AvatarInitial.Text = string.IsNullOrWhiteSpace(name) ? "?" : name.Substring(0, 1).ToUpperInvariant();
AvatarInitial.Visibility = System.Windows.Visibility.Visible;
}
}