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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:vm="clr-namespace:ytLive.ViewModels"
|
xmlns:vm="clr-namespace:ytLive.ViewModels"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="ytLive" Height="720" Width="1280"
|
Title="{Binding WindowTitle}" Height="720" Width="1280"
|
||||||
Background="#1a1a2e"
|
Background="#1a1a2e"
|
||||||
WindowStartupLocation="CenterScreen">
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
@@ -13,9 +13,26 @@
|
|||||||
<vm:MainViewModel/>
|
<vm:MainViewModel/>
|
||||||
</Window.DataContext>
|
</Window.DataContext>
|
||||||
|
|
||||||
|
<Window.TaskbarItemInfo>
|
||||||
|
<TaskbarItemInfo x:Name="TaskbarInfo"/>
|
||||||
|
</Window.TaskbarItemInfo>
|
||||||
|
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
<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 -->
|
<!-- Base button style -->
|
||||||
<Style x:Key="YtButton" TargetType="Button">
|
<Style x:Key="YtButton" TargetType="Button">
|
||||||
<Setter Property="Background" Value="#e94560"/>
|
<Setter Property="Background" Value="#e94560"/>
|
||||||
@@ -72,7 +89,7 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- ═══ TOP BAR: Stream Controls ═══ -->
|
<!-- ═══ 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>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
@@ -85,20 +102,23 @@
|
|||||||
Foreground="#e94560" FontSize="22" FontWeight="Bold"
|
Foreground="#e94560" FontSize="22" FontWeight="Bold"
|
||||||
VerticalAlignment="Center" Margin="0,0,24,0"/>
|
VerticalAlignment="Center" Margin="0,0,24,0"/>
|
||||||
|
|
||||||
<!-- Stream Title -->
|
<!-- LIVE badge (center) -->
|
||||||
<TextBox Grid.Column="1" Style="{StaticResource YtTextBox}"
|
<StackPanel Grid.Column="1" Orientation="Horizontal"
|
||||||
Text="{Binding StreamTitle, UpdateSourceTrigger=PropertyChanged}"
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
Width="400" HorizontalAlignment="Left"
|
Visibility="{Binding LiveIndicatorVisible, Converter={StaticResource BoolToVis}}">
|
||||||
VerticalAlignment="Center"/>
|
<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">
|
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
<Button Content="Connect" Style="{StaticResource YtButton}"
|
<Button Content="Start Stream" Style="{StaticResource YtButton}"
|
||||||
Command="{Binding ConnectCommand}"
|
Command="{Binding StartStreamCommand}"
|
||||||
Visibility="{Binding ShowConnect, Converter={StaticResource BoolToVis}}"/>
|
Visibility="{Binding IsOffline, Converter={StaticResource BoolToVis}}"/>
|
||||||
<Button Content="Go Live" Style="{StaticResource YtButton}"
|
|
||||||
Command="{Binding GoLiveCommand}"
|
|
||||||
Visibility="{Binding ShowGoLive, Converter={StaticResource BoolToVis}}"/>
|
|
||||||
<Button Content="End Stream" Style="{StaticResource YtButton}"
|
<Button Content="End Stream" Style="{StaticResource YtButton}"
|
||||||
Background="#333" Command="{Binding EndStreamCommand}"
|
Background="#333" Command="{Binding EndStreamCommand}"
|
||||||
Visibility="{Binding IsLive, Converter={StaticResource BoolToVis}}"/>
|
Visibility="{Binding IsLive, Converter={StaticResource BoolToVis}}"/>
|
||||||
@@ -151,7 +171,8 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- CENTER: Preview Area -->
|
<!-- 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>
|
<Grid>
|
||||||
<TextBlock Text="Preview" Foreground="#333" FontSize="24"
|
<TextBlock Text="Preview" Foreground="#333" FontSize="24"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
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;
|
namespace ytLive;
|
||||||
|
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
|
private readonly MainViewModel _viewModel;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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.
|
**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:
|
### Requirements:
|
||||||
|
|
||||||
@@ -25,8 +29,8 @@
|
|||||||
2. **Local HTTP listener** — `HttpListener` on `http://localhost:PORT/oauth2/callback` to catch the redirect
|
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
|
3. **Browser launch** — open the authorization URL in the default browser
|
||||||
4. **Token persistence** — store access/refresh tokens securely (Windows DPAPI), reload on startup
|
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
|
5. **UI state** — account shown in the Start Stream dialog; "Change Account" action triggers re-auth
|
||||||
6. **Go Live gated on auth** — "Go Live" button only visible once authenticated; scene building works without it
|
6. **Go Live gated on auth** — Start Stream dialog requires sign-in to enable the Start button; scene building works without it
|
||||||
|
|
||||||
### Tests:
|
### 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.Collections.ObjectModel;
|
||||||
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Threading;
|
||||||
using ytLive.Helpers;
|
using ytLive.Helpers;
|
||||||
using ytLive.Models;
|
using ytLive.Models;
|
||||||
using ytLive.Services;
|
using ytLive.Services;
|
||||||
@@ -11,13 +13,21 @@ public class MainViewModel : ViewModelBase
|
|||||||
private readonly YouTubeAuthService _youtubeAuth;
|
private readonly YouTubeAuthService _youtubeAuth;
|
||||||
private readonly YouTubeStreamService _youtubeStream;
|
private readonly YouTubeStreamService _youtubeStream;
|
||||||
private readonly YouTubeChatService _youtubeChat;
|
private readonly YouTubeChatService _youtubeChat;
|
||||||
|
private readonly DispatcherTimer _liveTimer;
|
||||||
|
|
||||||
private Scene? _activeScene;
|
private Scene? _activeScene;
|
||||||
private StreamStatus _streamStatus = StreamStatus.Offline;
|
private StreamStatus _streamStatus = StreamStatus.Offline;
|
||||||
private StreamHealth _currentHealth = new();
|
private StreamHealth _currentHealth = new();
|
||||||
private string _streamTitle = string.Empty;
|
private string _streamTitle = string.Empty;
|
||||||
private string _streamKey = string.Empty;
|
private string _streamDescription = string.Empty;
|
||||||
private bool _isYouTubeConnected;
|
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<Scene> Scenes { get; } = new();
|
||||||
public ObservableCollection<ChatMessage> ChatMessages { get; } = new();
|
public ObservableCollection<ChatMessage> ChatMessages { get; } = new();
|
||||||
@@ -37,13 +47,15 @@ public class MainViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsOffline));
|
OnPropertyChanged(nameof(IsOffline));
|
||||||
OnPropertyChanged(nameof(IsLive));
|
OnPropertyChanged(nameof(IsLive));
|
||||||
OnPropertyChanged(nameof(ShowGoLive));
|
OnPropertyChanged(nameof(LiveIndicatorVisible));
|
||||||
|
UpdateLiveVisuals();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsOffline => StreamStatus == StreamStatus.Offline;
|
public bool IsOffline => StreamStatus == StreamStatus.Offline;
|
||||||
public bool IsLive => StreamStatus == StreamStatus.Streaming;
|
public bool IsLive => StreamStatus == StreamStatus.Streaming;
|
||||||
|
public bool LiveIndicatorVisible => IsLive;
|
||||||
|
|
||||||
public StreamHealth CurrentHealth
|
public StreamHealth CurrentHealth
|
||||||
{
|
{
|
||||||
@@ -57,33 +69,58 @@ public class MainViewModel : ViewModelBase
|
|||||||
set => SetProperty(ref _streamTitle, value);
|
set => SetProperty(ref _streamTitle, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string StreamKey
|
public string StreamDescription
|
||||||
{
|
{
|
||||||
get => _streamKey;
|
get => _streamDescription;
|
||||||
set => SetProperty(ref _streamKey, value);
|
set => SetProperty(ref _streamDescription, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsYouTubeConnected
|
public string StreamVisibility
|
||||||
{
|
{
|
||||||
get => _isYouTubeConnected;
|
get => _streamVisibility;
|
||||||
set
|
set => SetProperty(ref _streamVisibility, value);
|
||||||
{
|
|
||||||
if (SetProperty(ref _isYouTubeConnected, value))
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(ShowConnect));
|
|
||||||
OnPropertyChanged(nameof(ShowGoLive));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShowConnect => !IsYouTubeConnected;
|
public string WindowTitle
|
||||||
public bool ShowGoLive => IsYouTubeConnected && IsOffline;
|
{
|
||||||
|
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
|
// Commands
|
||||||
public ICommand AddSceneCommand { get; }
|
public ICommand AddSceneCommand { get; }
|
||||||
public ICommand RemoveSceneCommand { get; }
|
public ICommand RemoveSceneCommand { get; }
|
||||||
public ICommand ConnectCommand { get; }
|
public ICommand StartStreamCommand { get; }
|
||||||
public ICommand GoLiveCommand { get; }
|
|
||||||
public ICommand EndStreamCommand { get; }
|
public ICommand EndStreamCommand { get; }
|
||||||
|
|
||||||
public MainViewModel()
|
public MainViewModel()
|
||||||
@@ -94,10 +131,12 @@ public class MainViewModel : ViewModelBase
|
|||||||
|
|
||||||
_youtubeChat.MessageReceived += OnChatMessageReceived;
|
_youtubeChat.MessageReceived += OnChatMessageReceived;
|
||||||
|
|
||||||
|
_liveTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||||
|
_liveTimer.Tick += OnLiveTimerTick;
|
||||||
|
|
||||||
AddSceneCommand = new RelayCommand(_ => AddScene());
|
AddSceneCommand = new RelayCommand(_ => AddScene());
|
||||||
RemoveSceneCommand = new RelayCommand(scene => RemoveScene(scene as Scene));
|
RemoveSceneCommand = new RelayCommand(scene => RemoveScene(scene as Scene));
|
||||||
ConnectCommand = new RelayCommand(_ => ConnectYouTube());
|
StartStreamCommand = new RelayCommand(_ => BeginGoLive());
|
||||||
GoLiveCommand = new RelayCommand(_ => StartStream(), _ => IsYouTubeConnected && IsOffline);
|
|
||||||
EndStreamCommand = new RelayCommand(_ => StopStream(), _ => IsLive);
|
EndStreamCommand = new RelayCommand(_ => StopStream(), _ => IsLive);
|
||||||
|
|
||||||
// Start with a default scene
|
// Start with a default scene
|
||||||
@@ -129,25 +168,54 @@ public class MainViewModel : ViewModelBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void StartStream()
|
private void BeginGoLive()
|
||||||
{
|
{
|
||||||
StreamStatus = StreamStatus.Connecting;
|
var dialog = new GoLiveViewModel();
|
||||||
|
var window = new ytLive.GoLiveWindow(dialog) { Owner = System.Windows.Application.Current.MainWindow };
|
||||||
// TODO: Initialize capture pipeline, encode, and push to RTMP
|
if (window.ShowDialog() == true)
|
||||||
// For now, simulate connection
|
{
|
||||||
await Task.Delay(500);
|
StreamTitle = dialog.StreamTitle;
|
||||||
StreamStatus = StreamStatus.Streaming;
|
StreamDescription = dialog.StreamDescription;
|
||||||
|
StreamVisibility = dialog.Visibility;
|
||||||
|
WindowTitle = string.IsNullOrWhiteSpace(dialog.StreamTitle)
|
||||||
|
? "ytLive"
|
||||||
|
: $"{dialog.StreamTitle} — ytLive";
|
||||||
|
StreamStatus = StreamStatus.Streaming;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StopStream()
|
private void StopStream()
|
||||||
{
|
{
|
||||||
StreamStatus = StreamStatus.Offline;
|
StreamStatus = StreamStatus.Offline;
|
||||||
|
WindowTitle = "ytLive";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConnectYouTube()
|
private void UpdateLiveVisuals()
|
||||||
{
|
{
|
||||||
// TODO: Launch OAuth2 flow in browser
|
var live = IsLive;
|
||||||
// For now, this is a stub
|
TopBarBackground = live ? "#e94560" : "#16213e";
|
||||||
IsYouTubeConnected = false;
|
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