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:
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user