Two-state flow with go-live dialog, unmissable live indicators, taskbar overlay

This commit is contained in:
2026-08-04 10:54:21 -07:00
parent aae17a1511
commit d6ae048583
7 changed files with 397 additions and 52 deletions
+101 -33
View File
@@ -1,5 +1,7 @@
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using ytLive.Helpers;
using ytLive.Models;
using ytLive.Services;
@@ -11,13 +13,21 @@ public class MainViewModel : ViewModelBase
private readonly YouTubeAuthService _youtubeAuth;
private readonly YouTubeStreamService _youtubeStream;
private readonly YouTubeChatService _youtubeChat;
private readonly DispatcherTimer _liveTimer;
private Scene? _activeScene;
private StreamStatus _streamStatus = StreamStatus.Offline;
private StreamHealth _currentHealth = new();
private string _streamTitle = string.Empty;
private string _streamKey = string.Empty;
private bool _isYouTubeConnected;
private string _streamDescription = string.Empty;
private string _streamVisibility = "Public";
private string _windowTitle = "ytLive";
private string _topBarBackground = "#16213e";
private string _previewGlowBrush = "Transparent";
private Thickness _previewGlowThickness = new(0);
private string _liveElapsedText = "00:00:00";
private double _livePulseOpacity = 1.0;
private TimeSpan _liveElapsed;
public ObservableCollection<Scene> Scenes { get; } = new();
public ObservableCollection<ChatMessage> ChatMessages { get; } = new();
@@ -37,13 +47,15 @@ public class MainViewModel : ViewModelBase
{
OnPropertyChanged(nameof(IsOffline));
OnPropertyChanged(nameof(IsLive));
OnPropertyChanged(nameof(ShowGoLive));
OnPropertyChanged(nameof(LiveIndicatorVisible));
UpdateLiveVisuals();
}
}
}
public bool IsOffline => StreamStatus == StreamStatus.Offline;
public bool IsLive => StreamStatus == StreamStatus.Streaming;
public bool LiveIndicatorVisible => IsLive;
public StreamHealth CurrentHealth
{
@@ -57,33 +69,58 @@ public class MainViewModel : ViewModelBase
set => SetProperty(ref _streamTitle, value);
}
public string StreamKey
public string StreamDescription
{
get => _streamKey;
set => SetProperty(ref _streamKey, value);
get => _streamDescription;
set => SetProperty(ref _streamDescription, value);
}
public bool IsYouTubeConnected
public string StreamVisibility
{
get => _isYouTubeConnected;
set
{
if (SetProperty(ref _isYouTubeConnected, value))
{
OnPropertyChanged(nameof(ShowConnect));
OnPropertyChanged(nameof(ShowGoLive));
}
}
get => _streamVisibility;
set => SetProperty(ref _streamVisibility, value);
}
public bool ShowConnect => !IsYouTubeConnected;
public bool ShowGoLive => IsYouTubeConnected && IsOffline;
public string WindowTitle
{
get => _windowTitle;
set => SetProperty(ref _windowTitle, value);
}
public string TopBarBackground
{
get => _topBarBackground;
set => SetProperty(ref _topBarBackground, value);
}
public string PreviewGlowBrush
{
get => _previewGlowBrush;
set => SetProperty(ref _previewGlowBrush, value);
}
public Thickness PreviewGlowThickness
{
get => _previewGlowThickness;
set => SetProperty(ref _previewGlowThickness, value);
}
public string LiveElapsedText
{
get => _liveElapsedText;
set => SetProperty(ref _liveElapsedText, value);
}
public double LivePulseOpacity
{
get => _livePulseOpacity;
set => SetProperty(ref _livePulseOpacity, value);
}
// Commands
public ICommand AddSceneCommand { get; }
public ICommand RemoveSceneCommand { get; }
public ICommand ConnectCommand { get; }
public ICommand GoLiveCommand { get; }
public ICommand StartStreamCommand { get; }
public ICommand EndStreamCommand { get; }
public MainViewModel()
@@ -94,10 +131,12 @@ public class MainViewModel : ViewModelBase
_youtubeChat.MessageReceived += OnChatMessageReceived;
_liveTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_liveTimer.Tick += OnLiveTimerTick;
AddSceneCommand = new RelayCommand(_ => AddScene());
RemoveSceneCommand = new RelayCommand(scene => RemoveScene(scene as Scene));
ConnectCommand = new RelayCommand(_ => ConnectYouTube());
GoLiveCommand = new RelayCommand(_ => StartStream(), _ => IsYouTubeConnected && IsOffline);
StartStreamCommand = new RelayCommand(_ => BeginGoLive());
EndStreamCommand = new RelayCommand(_ => StopStream(), _ => IsLive);
// Start with a default scene
@@ -129,25 +168,54 @@ public class MainViewModel : ViewModelBase
});
}
private async void StartStream()
private void BeginGoLive()
{
StreamStatus = StreamStatus.Connecting;
// TODO: Initialize capture pipeline, encode, and push to RTMP
// For now, simulate connection
await Task.Delay(500);
StreamStatus = StreamStatus.Streaming;
var dialog = new GoLiveViewModel();
var window = new ytLive.GoLiveWindow(dialog) { Owner = System.Windows.Application.Current.MainWindow };
if (window.ShowDialog() == true)
{
StreamTitle = dialog.StreamTitle;
StreamDescription = dialog.StreamDescription;
StreamVisibility = dialog.Visibility;
WindowTitle = string.IsNullOrWhiteSpace(dialog.StreamTitle)
? "ytLive"
: $"{dialog.StreamTitle} — ytLive";
StreamStatus = StreamStatus.Streaming;
}
}
private void StopStream()
{
StreamStatus = StreamStatus.Offline;
WindowTitle = "ytLive";
}
private void ConnectYouTube()
private void UpdateLiveVisuals()
{
// TODO: Launch OAuth2 flow in browser
// For now, this is a stub
IsYouTubeConnected = false;
var live = IsLive;
TopBarBackground = live ? "#e94560" : "#16213e";
PreviewGlowBrush = live ? "#e94560" : "Transparent";
PreviewGlowThickness = live ? new Thickness(3) : new Thickness(0);
if (live)
{
_liveElapsed = TimeSpan.Zero;
LiveElapsedText = "00:00:00";
LivePulseOpacity = 1.0;
_liveTimer.Start();
}
else
{
_liveTimer.Stop();
LiveElapsedText = "00:00:00";
LivePulseOpacity = 1.0;
}
}
private void OnLiveTimerTick(object? sender, EventArgs e)
{
_liveElapsed = _liveElapsed.Add(TimeSpan.FromSeconds(1));
LiveElapsedText = _liveElapsed.ToString(@"hh\:mm\:ss");
LivePulseOpacity = LivePulseOpacity > 0.5 ? 0.35 : 1.0;
}
}