Two-state flow with go-live dialog, unmissable live indicators, taskbar overlay
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
<Window x:Class="ytLive.GoLiveWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Start Stream" Height="430" Width="520"
|
||||
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
|
||||
ShowInTaskbar="False" Background="#1a1a2e">
|
||||
|
||||
<Window.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
|
||||
<Style x:Key="GoLiveButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="#e94560"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Padding" Value="16,8"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="4"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="GoLiveButtonSecondary" TargetType="Button" BasedOn="{StaticResource GoLiveButton}">
|
||||
<Setter Property="Background" Value="#16213e"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="GoLiveLabel" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="#a0a0b0"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Margin" Value="0,0,0,4"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="GoLiveTextBox" TargetType="TextBox">
|
||||
<Setter Property="Background" Value="#0f3460"/>
|
||||
<Setter Property="Foreground" Value="#e0e0e0"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="8,6"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="CaretBrush" Value="White"/>
|
||||
<Setter Property="Margin" Value="0,0,0,12"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="GoLiveCombo" TargetType="ComboBox">
|
||||
<Setter Property="Background" Value="#0f3460"/>
|
||||
<Setter Property="Foreground" Value="#e0e0e0"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="8,6"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Margin" Value="0,0,0,12"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="24">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Text="Start Stream" FontSize="18" FontWeight="Bold"
|
||||
Foreground="#e0e0e0" Margin="0,0,0,16"/>
|
||||
|
||||
<!-- ACCOUNT -->
|
||||
<StackPanel Grid.Row="1" Margin="0,0,0,16">
|
||||
<TextBlock Text="YOUTUBE ACCOUNT" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="#a0a0b0" Margin="0,0,0,8"/>
|
||||
|
||||
<!-- Not signed in -->
|
||||
<StackPanel Visibility="{Binding ShowSignIn, Converter={StaticResource BoolToVis}}">
|
||||
<TextBlock Text="Sign in to connect your channel." Style="{StaticResource GoLiveLabel}"/>
|
||||
<Button Content="Sign In" Command="{Binding SignInCommand}"
|
||||
Style="{StaticResource GoLiveButtonSecondary}" HorizontalAlignment="Left"
|
||||
Padding="20,8" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Signed in: saved account as default + change option -->
|
||||
<StackPanel Visibility="{Binding ShowAccount, Converter={StaticResource BoolToVis}}">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<TextBlock Text="{Binding AccountName}" FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="#e0e0e0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="(signed in)" Foreground="#7fb069" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0"/>
|
||||
</StackPanel>
|
||||
<Button Content="Change Account" Command="{Binding ChangeAccountCommand}"
|
||||
Style="{StaticResource GoLiveButtonSecondary}" HorizontalAlignment="Left"
|
||||
Padding="16,6" Margin="0,4,0,0" FontSize="12"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- STREAM METADATA -->
|
||||
<StackPanel Grid.Row="2">
|
||||
<TextBlock Text="STREAM DETAILS" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="#a0a0b0" Margin="0,0,0,8"/>
|
||||
|
||||
<TextBlock Text="Title" Style="{StaticResource GoLiveLabel}"/>
|
||||
<TextBox Style="{StaticResource GoLiveTextBox}"
|
||||
Text="{Binding StreamTitle, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
|
||||
<TextBlock Text="Description" Style="{StaticResource GoLiveLabel}"/>
|
||||
<TextBox Style="{StaticResource GoLiveTextBox}" Height="60" AcceptsReturn="True"
|
||||
Text="{Binding StreamDescription, UpdateSourceTrigger=PropertyChanged}"
|
||||
VerticalScrollBarVisibility="Auto"/>
|
||||
|
||||
<TextBlock Text="Visibility" Style="{StaticResource GoLiveLabel}"/>
|
||||
<ComboBox Style="{StaticResource GoLiveCombo}"
|
||||
ItemsSource="{Binding Visibilities}"
|
||||
SelectedItem="{Binding Visibility}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ACTIONS -->
|
||||
<StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="Cancel" Command="{Binding CancelCommand}"
|
||||
Style="{StaticResource GoLiveButtonSecondary}" Margin="0,0,8,0"/>
|
||||
<Button Content="Start Stream" Command="{Binding StartCommand}"
|
||||
Style="{StaticResource GoLiveButton}" MinWidth="110"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Windows;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive;
|
||||
|
||||
public partial class GoLiveWindow : Window
|
||||
{
|
||||
public GoLiveWindow(GoLiveViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = viewModel;
|
||||
viewModel.StartRequested += () => DialogResult = true;
|
||||
viewModel.CancelRequested += () => DialogResult = false;
|
||||
}
|
||||
}
|
||||
+36
-15
@@ -5,7 +5,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:ytLive.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
Title="ytLive" Height="720" Width="1280"
|
||||
Title="{Binding WindowTitle}" Height="720" Width="1280"
|
||||
Background="#1a1a2e"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
|
||||
@@ -13,9 +13,26 @@
|
||||
<vm:MainViewModel/>
|
||||
</Window.DataContext>
|
||||
|
||||
<Window.TaskbarItemInfo>
|
||||
<TaskbarItemInfo x:Name="TaskbarInfo"/>
|
||||
</Window.TaskbarItemInfo>
|
||||
|
||||
<Window.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
|
||||
<!-- Red dot shown in the taskbar icon while live -->
|
||||
<DrawingImage x:Key="LiveOverlay">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<GeometryDrawing Brush="#e94560">
|
||||
<GeometryDrawing.Geometry>
|
||||
<EllipseGeometry Center="8,8" RadiusX="8" RadiusY="8"/>
|
||||
</GeometryDrawing.Geometry>
|
||||
</GeometryDrawing>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
|
||||
<!-- Base button style -->
|
||||
<Style x:Key="YtButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="#e94560"/>
|
||||
@@ -72,7 +89,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ═══ TOP BAR: Stream Controls ═══ -->
|
||||
<Border Grid.Row="0" Background="#16213e" Padding="16,10">
|
||||
<Border Grid.Row="0" Background="{Binding TopBarBackground}" Padding="16,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
@@ -85,20 +102,23 @@
|
||||
Foreground="#e94560" FontSize="22" FontWeight="Bold"
|
||||
VerticalAlignment="Center" Margin="0,0,24,0"/>
|
||||
|
||||
<!-- Stream Title -->
|
||||
<TextBox Grid.Column="1" Style="{StaticResource YtTextBox}"
|
||||
Text="{Binding StreamTitle, UpdateSourceTrigger=PropertyChanged}"
|
||||
Width="400" HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"/>
|
||||
<!-- LIVE badge (center) -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Visibility="{Binding LiveIndicatorVisible, Converter={StaticResource BoolToVis}}">
|
||||
<Ellipse Width="12" Height="12" Fill="White" VerticalAlignment="Center"
|
||||
Opacity="{Binding LivePulseOpacity}"/>
|
||||
<TextBlock Text="LIVE" Foreground="White" FontSize="16" FontWeight="Bold"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0"/>
|
||||
<TextBlock Text="{Binding LiveElapsedText}" Foreground="White" FontSize="14"
|
||||
FontFamily="Consolas" VerticalAlignment="Center" Margin="14,0,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Primary actions: one relevant button per state -->
|
||||
<!-- Single two-state action button -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button Content="Connect" Style="{StaticResource YtButton}"
|
||||
Command="{Binding ConnectCommand}"
|
||||
Visibility="{Binding ShowConnect, Converter={StaticResource BoolToVis}}"/>
|
||||
<Button Content="Go Live" Style="{StaticResource YtButton}"
|
||||
Command="{Binding GoLiveCommand}"
|
||||
Visibility="{Binding ShowGoLive, Converter={StaticResource BoolToVis}}"/>
|
||||
<Button Content="Start Stream" Style="{StaticResource YtButton}"
|
||||
Command="{Binding StartStreamCommand}"
|
||||
Visibility="{Binding IsOffline, Converter={StaticResource BoolToVis}}"/>
|
||||
<Button Content="End Stream" Style="{StaticResource YtButton}"
|
||||
Background="#333" Command="{Binding EndStreamCommand}"
|
||||
Visibility="{Binding IsLive, Converter={StaticResource BoolToVis}}"/>
|
||||
@@ -151,7 +171,8 @@
|
||||
</Border>
|
||||
|
||||
<!-- CENTER: Preview Area -->
|
||||
<Border Grid.Column="1" Background="#0a0a1a" CornerRadius="6" Margin="8,0">
|
||||
<Border Grid.Column="1" Background="#0a0a1a" CornerRadius="6" Margin="8,0"
|
||||
BorderBrush="{Binding PreviewGlowBrush}" BorderThickness="{Binding PreviewGlowThickness}">
|
||||
<Grid>
|
||||
<TextBlock Text="Preview" Foreground="#333" FontSize="24"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
|
||||
+22
-1
@@ -1,11 +1,32 @@
|
||||
using System.Windows;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly MainViewModel _viewModel;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = (MainViewModel)DataContext;
|
||||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
UpdateTaskbarOverlay();
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(MainViewModel.IsLive))
|
||||
UpdateTaskbarOverlay();
|
||||
}
|
||||
|
||||
private void UpdateTaskbarOverlay()
|
||||
{
|
||||
TaskbarInfo.Overlay = _viewModel.IsLive
|
||||
? (ImageSource)FindResource("LiveOverlay")
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
|
||||
**Goal:** Fully working Google OAuth2 flow — user clicks "YouTube", browser opens, authorization callback lands, channel info is stored.
|
||||
|
||||
**Design constraint:** Sign-in must NEVER block core exploration. Users can build scenes, add sources, and audition the software without authenticating. But **going live requires authentication** — "Go Live" only appears once connected. OAuth is what unlocks the actual broadcast (plus live chat, scheduling, and YouTube-side health).
|
||||
**Design constraint:** Sign-in must NEVER block core exploration. Users can build scenes, add sources, and audition the software without authenticating. But **going live requires authentication** — the "Start Stream" dialog is where the account sign-in lives, alongside all stream metadata.
|
||||
|
||||
**Two-state flow:** There is no separate "Connect" button. The top bar shows a single button — **Start Stream** when idle, **End Stream** when live. Clicking Start Stream opens one dialog that supplies everything: account (previously-saved account shown as default, with a Change Account action) + title/description/visibility.
|
||||
|
||||
**Live indicators (unmissable):** Top bar + window title bar flip red, a pulsing **● LIVE** badge with elapsed timer appears in the top bar, the preview area gets a red glow, and the taskbar icon shows a red overlay dot. Window title bar shows the stream title once validated.
|
||||
|
||||
### Requirements:
|
||||
|
||||
@@ -25,8 +29,8 @@
|
||||
2. **Local HTTP listener** — `HttpListener` on `http://localhost:PORT/oauth2/callback` to catch the redirect
|
||||
3. **Browser launch** — open the authorization URL in the default browser
|
||||
4. **Token persistence** — store access/refresh tokens securely (Windows DPAPI), reload on startup
|
||||
5. **UI state** — account name/avatar shown when connected; "sign in (optional)" affordance, never a gate
|
||||
6. **Go Live gated on auth** — "Go Live" button only visible once authenticated; scene building works without it
|
||||
5. **UI state** — account shown in the Start Stream dialog; "Change Account" action triggers re-auth
|
||||
6. **Go Live gated on auth** — Start Stream dialog requires sign-in to enable the Start button; scene building works without it
|
||||
|
||||
### Tests:
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Windows.Input;
|
||||
using ytLive.Helpers;
|
||||
|
||||
namespace ytLive.ViewModels;
|
||||
|
||||
public class GoLiveViewModel : ViewModelBase
|
||||
{
|
||||
private bool _isSignedIn;
|
||||
private string _accountName = string.Empty;
|
||||
private string _streamTitle = string.Empty;
|
||||
private string _streamDescription = string.Empty;
|
||||
private string _visibility = "Public";
|
||||
|
||||
public bool IsSignedIn
|
||||
{
|
||||
get => _isSignedIn;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isSignedIn, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(ShowSignIn));
|
||||
OnPropertyChanged(nameof(ShowAccount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowSignIn => !IsSignedIn;
|
||||
public bool ShowAccount => IsSignedIn;
|
||||
|
||||
public string AccountName
|
||||
{
|
||||
get => _accountName;
|
||||
set => SetProperty(ref _accountName, value);
|
||||
}
|
||||
|
||||
public string StreamTitle
|
||||
{
|
||||
get => _streamTitle;
|
||||
set => SetProperty(ref _streamTitle, value);
|
||||
}
|
||||
|
||||
public string StreamDescription
|
||||
{
|
||||
get => _streamDescription;
|
||||
set => SetProperty(ref _streamDescription, value);
|
||||
}
|
||||
|
||||
public string Visibility
|
||||
{
|
||||
get => _visibility;
|
||||
set => SetProperty(ref _visibility, value);
|
||||
}
|
||||
|
||||
public string[] Visibilities { get; } = { "Public", "Unlisted", "Private" };
|
||||
|
||||
public ICommand SignInCommand { get; }
|
||||
public ICommand ChangeAccountCommand { get; }
|
||||
public ICommand StartCommand { get; }
|
||||
public ICommand CancelCommand { get; }
|
||||
|
||||
public GoLiveViewModel()
|
||||
{
|
||||
SignInCommand = new RelayCommand(_ => SignIn());
|
||||
ChangeAccountCommand = new RelayCommand(_ => ChangeAccount());
|
||||
StartCommand = new RelayCommand(_ => StartRequested?.Invoke(), _ => IsSignedIn);
|
||||
CancelCommand = new RelayCommand(_ => CancelRequested?.Invoke());
|
||||
}
|
||||
|
||||
public event Action? StartRequested;
|
||||
public event Action? CancelRequested;
|
||||
|
||||
private void SignIn()
|
||||
{
|
||||
// TODO: OAuth2 flow; for now simulate a successful sign-in
|
||||
AccountName = "Connected Channel";
|
||||
IsSignedIn = true;
|
||||
}
|
||||
|
||||
private void ChangeAccount()
|
||||
{
|
||||
// TODO: re-run OAuth2; for now simulate signing out
|
||||
IsSignedIn = false;
|
||||
AccountName = string.Empty;
|
||||
}
|
||||
}
|
||||
+101
-33
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user