diff --git a/Controls/TopBar.xaml b/Controls/TopBar.xaml
new file mode 100644
index 0000000..d53a6f2
--- /dev/null
+++ b/Controls/TopBar.xaml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Controls/TopBar.xaml.cs b/Controls/TopBar.xaml.cs
new file mode 100644
index 0000000..2e1ef25
--- /dev/null
+++ b/Controls/TopBar.xaml.cs
@@ -0,0 +1,72 @@
+using System;
+using System.IO;
+using System.Net.Http;
+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 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;
+ }
+}
\ No newline at end of file
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 4999fbe..8e8f28a 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -76,119 +76,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index afe5cc7..b111425 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.ComponentModel;
-using System.IO;
using System.Net.Http;
using System.Windows;
using System.Windows.Controls;
@@ -22,6 +21,7 @@ public partial class MainWindow : Window
{
private readonly MainViewModel _viewModel;
private readonly Dictionary _activeBorderAnimations = new();
+ private Controls.TopBar _topBar = null!;
public MainWindow()
{
@@ -34,8 +34,8 @@ public partial class MainWindow : Window
_viewModel.ChatMessages.CollectionChanged += OnChatMessagesChanged;
UpdateTaskbarOverlay();
UpdateSelectionOverlay();
- AvatarImage.Source = null;
- AvatarInitial.Visibility = Visibility.Collapsed;
+ _topBar = (Controls.TopBar)FindName("TopBar")!;
+ _topBar.RefreshAvatar();
RestoreWindowGeometry();
AppLog.Write("MainWindow ctor: end");
}
@@ -116,7 +116,7 @@ public partial class MainWindow : Window
UpdateSelectionOverlay();
else if (e.PropertyName == nameof(MainViewModel.AccountAvatarUrl)
|| e.PropertyName == nameof(MainViewModel.IsConnected))
- LoadAvatarImage();
+ _topBar.RefreshAvatar();
}
private void MainWindow_Closing(object? sender, CancelEventArgs e)
@@ -141,53 +141,6 @@ public partial class MainWindow : Window
: null;
}
- private static readonly HttpClient AvatarHttp = new() { Timeout = TimeSpan.FromSeconds(15) };
- private string? _loadedAvatarUrl;
-
- private async void LoadAvatarImage()
- {
- var url = _viewModel.AccountAvatarUrl;
- if (!_viewModel.IsConnected)
- {
- 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 = 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(name) ? "?" : name.Substring(0, 1).ToUpperInvariant();
- AvatarInitial.Visibility = Visibility.Visible;
- }
-
private void SourceNameBox_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (sender is TextBox { IsVisible: true } box)
diff --git a/ViewModels/index.md b/ViewModels/index.md
index 006ccc1..e185ded 100644
--- a/ViewModels/index.md
+++ b/ViewModels/index.md
@@ -55,7 +55,8 @@ Progress (Commit N/12 · tag):
**Phase 2 — `MainWindow.xaml` split into UserControls** (new `Controls/` folder, namespace `ytLive.Controls`):
- [x] **Commit 12 · `refactor-commit-12`** — **BottomBar** → `Controls/BottomBar.xaml(.cs)` (gear menu + stream stats + quality dropdown; `GearButton_Click` moved to the control's code-behind).
-- [x] **Commit 13 · `refactor-commit-13`** — **HealthBanner** → `Controls/HealthBanner.xaml` (Row 1). Also promoted the 7 value↔visibility converters out of `MainWindow.Window.Resources` into `Themes/Controls.xaml` so extracted controls can resolve them (app-level, not window-level). Pending: TopBar, left scene/chat panel, preview pane, overlay host.
+- [x] **Commit 13 · `refactor-commit-13`** — **HealthBanner** → `Controls/HealthBanner.xaml` (Row 1). Also promoted the 7 value↔visibility converters out of `MainWindow.Window.Resources` into `Themes/Controls.xaml` so extracted controls can resolve them (app-level, not window-level).
+- [x] **Commit 14 · `refactor-commit-14`** — **TopBar** → `Controls/TopBar.xaml(.cs)` (Row 0: logo, REC/ON-AIR pills + signs, avatar, Sign In/Start/End buttons). Avatar fetch moved (async-preserved) into the control; window exposes `_topBar.RefreshAvatar()` invoked on `IsConnected`/`AccountAvatarUrl` changes. Pending: left scene/chat panel, preview pane, overlay host.
The partial `.cs` files land next to `MainViewModel.cs` in this folder as the split
proceeds; each partial carries its own `using`s and re-declares nothing from core.
\ No newline at end of file