OAuth session persists via DPAPI; sign-in lives in the Start Stream dialog; add xUnit test project (6 passing)

This commit is contained in:
2026-08-06 09:38:35 -07:00
parent 8f0ecd3796
commit 4afe11e718
15 changed files with 432 additions and 67 deletions
+42 -11
View File
@@ -126,18 +126,14 @@ public class MainViewModel : ViewModelBase
set
{
if (SetProperty(ref _isConnected, value))
{
OnPropertyChanged(nameof(IsNotConnected));
OnPropertyChanged(nameof(ShowStartStream));
}
}
}
public bool IsOffline => StreamStatus == StreamStatus.Offline;
public bool IsLive => StreamStatus == StreamStatus.Streaming;
public bool LiveIndicatorVisible => IsLive;
public bool IsNotConnected => !IsConnected;
public bool ShowStartStream => IsConnected && IsOffline;
public bool ShowStartStream => IsOffline;
public bool ShowChatInactiveMessage => !IsLive;
public bool ShowEmptySceneHint => ActiveScene != null && ActiveScene.Sources.Count == 0;
public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null;
@@ -406,7 +402,6 @@ public class MainViewModel : ViewModelBase
public ICommand AddSourceCommand { get; }
public ICommand AddImageCommand { get; }
public ICommand RemoveSourceCommand { get; }
public ICommand ConnectCommand { get; }
public ICommand StartStreamCommand { get; }
public ICommand EndStreamCommand { get; }
public ICommand OpenSettingsCommand { get; }
@@ -424,7 +419,8 @@ public class MainViewModel : ViewModelBase
public MainViewModel()
{
AppLog.Write("MainViewModel ctor begin");
_youtubeAuth = new YouTubeAuthService(OAuthCredentials.ClientId, OAuthCredentials.ClientSecret);
_youtubeAuth = new YouTubeAuthService(OAuthCredentials.ClientId, OAuthCredentials.ClientSecret,
sessionChanged: ch => TokenStore.Save(ch));
_youtubeStream = new YouTubeStreamService(_youtubeAuth);
_youtubeChat = new YouTubeChatService(_youtubeAuth);
AppLog.Write("MainViewModel ctor: services created");
@@ -462,7 +458,6 @@ public class MainViewModel : ViewModelBase
SubmitBugCommand = new RelayCommand(_ => SubmitBug());
SubmitFeatureCommand = new RelayCommand(_ => SubmitFeature());
OpenChannelCommand = new RelayCommand(_ => OpenUrl(ChannelUrl));
ConnectCommand = new RelayCommand(_ => _ = ConnectAsync());
StartStreamCommand = new RelayCommand(_ => BeginGoLive());
EndStreamCommand = new RelayCommand(_ => StopStream(), _ => IsLive);
SaveLayoutCommand = new RelayCommand(_ => SaveLayoutNow());
@@ -472,9 +467,40 @@ public class MainViewModel : ViewModelBase
_layoutStore = new LayoutStore(DefaultLayoutPath);
_activeLayoutPath = _layoutStore.ActivePath;
LoadLayout();
_ = LoadSavedSessionAsync();
AppLog.Write("MainViewModel ctor end");
}
// Restores the DPAPI-saved OAuth session so sign-in survives restarts.
// Best-effort: refresh a near-expiry access token; a session that can no
// longer refresh is discarded and the user signs in again.
private async Task LoadSavedSessionAsync()
{
try
{
var saved = TokenStore.Load();
if (saved == null) return;
_youtubeAuth.SetSession(saved);
if (saved.TokenExpiry <= DateTime.UtcNow.AddMinutes(5))
{
var refreshed = await Task.Run(() => _youtubeAuth.RefreshToken());
if (!refreshed)
{
TokenStore.Clear();
AppLog.Write("Saved session could not be refreshed; signed out");
return;
}
}
IsConnected = true;
}
catch (Exception ex)
{
AppLog.Write($"LoadSavedSessionAsync failed: {ex}");
}
}
private static string DefaultLayoutPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"ytLlive",
@@ -870,7 +896,10 @@ public class MainViewModel : ViewModelBase
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
private async Task ConnectAsync()
// Runs the OAuth flow (browser + loopback callback) on a background thread.
// The resulting channel is returned to the caller and persisted via the
// auth service's sessionChanged hook.
private async Task<YouTubeChannel?> SignInAsync()
{
try
{
@@ -879,15 +908,17 @@ public class MainViewModel : ViewModelBase
{
MessageBox.Show("Sign-in was unsuccessful. Please try again.", "ytLlive",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
return null;
}
IsConnected = true;
return channel;
}
catch (Exception ex)
{
MessageBox.Show($"Sign-in failed: {ex.Message}", "ytLlive",
MessageBoxButton.OK, MessageBoxImage.Error);
return null;
}
}
@@ -903,7 +934,7 @@ public class MainViewModel : ViewModelBase
private void BeginGoLive()
{
var dialog = new GoLiveViewModel
var dialog = new GoLiveViewModel(() => SignInAsync(), _youtubeAuth.CurrentChannel)
{
StreamTitle = DefaultStreamTitle,
StreamDescription = DefaultStreamDescription,