Audio UX (UI) + bottom bar rework: two-line footer (dropped/duration under bitrate/fps), centered sound meter (blank→green→yellow→red, zone markers at 60%/80%) + MIC chip (MicVolume slider, ToggleMicMuteCommand) — the mic is the creator's only audio control, desktop/game audio is automatic (KISS rule); scenes list is content-height (no dead space before SOURCES); top bar shows the connected YT account avatar/name via SyncConnectedAccount; docs updated (65 tests passing)

This commit is contained in:
2026-08-07 15:20:48 -07:00
parent 4d10b23714
commit 4b3fb04322
5 changed files with 183 additions and 55 deletions
+76
View File
@@ -27,6 +27,11 @@ public class MainViewModel : ViewModelBase
private SceneElement? _selectedElement;
private ImageSource? _activeBackgroundImage;
private bool _isConnected;
private string _accountAvatarUrl = string.Empty;
private string _accountDisplayName = string.Empty;
private double _audioLevel;
private double _micVolume = 1.0;
private bool _micMuted;
private StreamStatus _streamStatus = StreamStatus.Offline;
private StreamHealth _currentHealth = new();
private string _streamTitle = string.Empty;
@@ -187,6 +192,65 @@ public class MainViewModel : ViewModelBase
}
}
/// <summary>The connected YouTube account's avatar + name — shown in the top
/// bar so the creator always sees WHICH account is about to go live.</summary>
public string AccountAvatarUrl
{
get => _accountAvatarUrl;
private set => SetProperty(ref _accountAvatarUrl, value);
}
public string AccountDisplayName
{
get => _accountDisplayName;
private set => SetProperty(ref _accountDisplayName, value);
}
// ─── Audio (KISS: desktop/game audio is automatic — zero UI. The creator's
// ─── only audio control is the mic: meter + volume + mute.) ───
/// <summary>Current level 0..1 — fed by the audio mixer once capture lands.
/// Drives the sound meter's fill width + zone color.</summary>
public double AudioLevel
{
get => _audioLevel;
set
{
if (SetProperty(ref _audioLevel, Math.Clamp(value, 0, 1)))
{
OnPropertyChanged(nameof(MeterFillWidth));
OnPropertyChanged(nameof(MeterBrush));
}
}
}
public double MeterFillWidth => AudioLevel * 360;
public string MeterBrush => AudioLevel switch
{
< 0.6 => "#22c55e",
< 0.8 => "#eab308",
_ => "#ef4444",
};
public double MicVolume
{
get => _micVolume;
set => SetProperty(ref _micVolume, Math.Clamp(value, 0, 1));
}
public bool MicMuted
{
get => _micMuted;
set
{
if (SetProperty(ref _micMuted, value))
OnPropertyChanged(nameof(MicMuteText));
}
}
public string MicMuteText => MicMuted ? "Unmute" : "Mute";
public bool IsOffline => StreamStatus == StreamStatus.Offline;
public bool IsLive => StreamStatus == StreamStatus.Streaming;
public bool LiveIndicatorVisible => IsLive;
@@ -533,6 +597,7 @@ public class MainViewModel : ViewModelBase
public ICommand ChangeCaptureCommand { get; }
public ICommand RefreshCaptureCommand { get; }
public ICommand SetBackdropDisplayCommand { get; }
public ICommand ToggleMicMuteCommand { get; }
public ICommand StartStreamCommand { get; }
public ICommand EndStreamCommand { get; }
public ICommand OpenSettingsCommand { get; }
@@ -587,6 +652,7 @@ public class MainViewModel : ViewModelBase
ChangeCaptureCommand = new RelayCommand(_ => _ = ChangeBackdropCaptureAsync());
RefreshCaptureCommand = new RelayCommand(_ => RefreshBackdropAutoCapture());
SetBackdropDisplayCommand = new RelayCommand(display => SetBackdropCapture(display as DisplayInfo));
ToggleMicMuteCommand = new RelayCommand(_ => MicMuted = !MicMuted);
OpenSettingsCommand = new RelayCommand(_ => ShowOverlay(nameof(IsSettingsOpen), "App Settings"));
OpenBugCommand = new RelayCommand(_ => ShowOverlay(nameof(IsBugOpen), "Report Bug"));
OpenFeatureCommand = new RelayCommand(_ => ShowOverlay(nameof(IsFeatureOpen), "Feature Request"));
@@ -651,6 +717,7 @@ public class MainViewModel : ViewModelBase
}
IsConnected = true;
SyncConnectedAccount();
}
catch (Exception ex)
{
@@ -658,6 +725,13 @@ public class MainViewModel : ViewModelBase
}
}
private void SyncConnectedAccount()
{
var channel = _youtubeAuth.CurrentChannel;
AccountAvatarUrl = channel?.ProfileImageUrl ?? string.Empty;
AccountDisplayName = channel?.DisplayName ?? string.Empty;
}
private static string DefaultLayoutPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"ytLlive",
@@ -1443,6 +1517,7 @@ public class MainViewModel : ViewModelBase
}
IsConnected = true;
SyncConnectedAccount();
return channel;
}
catch (Exception ex)
@@ -1494,6 +1569,7 @@ public class MainViewModel : ViewModelBase
_youtubeAuth.ClearSession();
TokenStore.Clear();
IsConnected = false;
SyncConnectedAccount();
AppLog.Write("Stream ended; session signed out");
}