refactor: extract TopBar out of MainWindow.xaml — Phase 2, Commit 14
TopBar.xaml(.cs) takes the Row 0 top controls: logo/About button, REC/ON-AIR pills + status signs, avatar, Sign In/Start/End buttons. The avatar fetch (AvatarHttp/_loadedAvatarUrl/LoadAvatarImage/ShowAvatarFallback) moved into the control with async semantics preserved; the window grabs the control via FindName in its ctor and calls _topBar.RefreshAvatar() when IsConnected or AccountAvatarUrl changes (previously OnViewModelPropertyChanged -> LoadAvatarImage). AccountLight is pure binding (never touched by code-behind) so it moved with the XAML. Zero behavior change; build 0 warnings; 246 pass, only the 2 known failures. Also dropped the now-unused System.IO using from MainWindow.xaml.cs. Docs: ViewModels/index.md Phase 2 tracker updated in the same commit.
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
<UserControl x:Class="ytLive.Controls.TopBar"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Name="TopBarRoot">
|
||||
<!-- LOGO / BRAND — clicking opens the in-app About overlay -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Logo / Brand — clicking opens the in-app About overlay -->
|
||||
<Button Grid.Column="0" Background="Transparent" BorderThickness="0" Cursor="Hand"
|
||||
VerticalAlignment="Center" Margin="0,0,24,0" ToolTip="About LlamaCasty"
|
||||
Command="{Binding OpenAboutCommand}">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<ContentPresenter/>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
<TextBlock FontSize="22" FontWeight="Bold">
|
||||
<Run Text="LlamaCas" Foreground="#e0e0e0"/><Run Text="ty" Foreground="#e94560"/>
|
||||
</TextBlock>
|
||||
</Button>
|
||||
|
||||
<!-- REC / ON-AIR pills (intent) + status signs (reality) -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<!-- REC pill: local recording needs no sign-in -->
|
||||
<TextBlock Text="REC" Foreground="#e94560" FontWeight="Bold" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<ToggleButton Style="{StaticResource PillToggle}" VerticalAlignment="Center"
|
||||
IsChecked="{Binding RecordPillOn}"/>
|
||||
<!-- ON-AIR pill: greyed out until signed in -->
|
||||
<TextBlock Text="ON-AIR" Foreground="#e94560" FontWeight="Bold" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="16,0,6,0"/>
|
||||
<ToggleButton Style="{StaticResource PillToggle}" VerticalAlignment="Center"
|
||||
IsChecked="{Binding OnAirPillOn}"
|
||||
IsEnabled="{Binding CanToggleOnAir}"
|
||||
ToolTip="{Binding AccountStatusLightToolTip}"/>
|
||||
<!-- Status signs: REC dot + ON-AIR dot -->
|
||||
<Ellipse Width="12" Height="12" VerticalAlignment="Center" Margin="16,0,0,0"
|
||||
Fill="{Binding RecDotBrush}" Opacity="{Binding RecDotOpacity}"/>
|
||||
<TextBlock Text="REC" Style="{StaticResource StatusSignText}"/>
|
||||
<!-- ON-AIR sign: gray offline, red public, green private -->
|
||||
<Ellipse Width="12" Height="12" VerticalAlignment="Center" Margin="16,0,0,0"
|
||||
Fill="{Binding OnAirBrush}"/>
|
||||
<TextBlock Text="ON-AIR" Style="{StaticResource StatusSignText}"/>
|
||||
<Border Background="#8f1f1f" CornerRadius="3" Padding="6,1" Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding IsLivePrivate, Converter={StaticResource BoolToVis}}">
|
||||
<TextBlock Text="PRIVATE" FontSize="11" FontWeight="Bold" Foreground="White"/>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding LiveElapsedText}" Foreground="White" FontSize="14"
|
||||
FontFamily="Consolas" VerticalAlignment="Center" Margin="14,0,0,0"
|
||||
Visibility="{Binding IsLive, Converter={StaticResource BoolToVis}}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Three-state top bar: logged-out / idle / live -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<!-- Status light -->
|
||||
<Ellipse Width="8" Height="8" Margin="0,0,8,0" VerticalAlignment="Center"
|
||||
ToolTip="{Binding AccountStatusLightToolTip}">
|
||||
<Ellipse.Style>
|
||||
<Style TargetType="Ellipse">
|
||||
<Setter Property="Fill" Value="#ef4444"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Fill" Value="#22c55e"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsLive}" Value="True">
|
||||
<Setter Property="Fill" Value="#e94560"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Ellipse.Style>
|
||||
</Ellipse>
|
||||
<!-- 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}}">
|
||||
<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>
|
||||
<!-- Sign In button (disconnected + offline) -->
|
||||
<Button Content="Sign In" Style="{StaticResource YtButton}"
|
||||
Margin="0,0,12,0"
|
||||
Command="{Binding StartStreamCommand}"
|
||||
Visibility="{Binding ShowLogInButton, Converter={StaticResource BoolToVis}}"/>
|
||||
<!-- Primary Start button (offline): dynamic label from the pills -->
|
||||
<Button Content="Start" Style="{StaticResource YtButton}"
|
||||
Command="{Binding StartStreamCommand}"
|
||||
Visibility="{Binding ShowPrimaryStartButton, Converter={StaticResource BoolToVis}}">
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Choose Record Folder" Command="{Binding ChooseRecordFolderCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="Change Account" Command="{Binding ChangeAccountCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="Logout" Command="{Binding LogoutCommand}"/>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
</Button>
|
||||
<!-- End button (live OR recording) — the only button whose
|
||||
label changes; "Stop Recording" while recording -->
|
||||
<Button Content="{Binding EndButtonText}" Style="{StaticResource YtButton}"
|
||||
Background="#333" Command="{Binding EndStreamCommand}"
|
||||
Visibility="{Binding ShowEndStreamButton, Converter={StaticResource BoolToVis}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user