using ytLive.Helpers; using ytLive.Models; using ytLive.Services; namespace ytLive.ViewModels; public partial class MainViewModel { private bool _isConnected; private string _accountAvatarUrl = string.Empty; private string _accountDisplayName = string.Empty; public bool IsConnected { get => _isConnected; set { if (SetProperty(ref _isConnected, value)) { // ON-AIR requires a sign-in; dropping the session turns the pill // off so the state can never offer a stream without an account. if (!value) OnAirPillOn = false; OnPropertyChanged(nameof(PrimaryStartButtonLabel)); OnPropertyChanged(nameof(ShowPrimaryStartButton)); OnPropertyChanged(nameof(AccountStatusLightToolTip)); } } } /// The connected YouTube account's avatar + name — shown in the top /// bar so the creator always sees WHICH account is about to go live. public string AccountAvatarUrl { get => _accountAvatarUrl; private set => SetProperty(ref _accountAvatarUrl, value); } public string AccountDisplayName { get => _accountDisplayName; private set => SetProperty(ref _accountDisplayName, value); } /// Tooltip for the account status light explaining the color. public string AccountStatusLightToolTip => IsLive ? "Live on YouTube — the top bar is red to show the stream is on the air." : IsConnected ? "You're signed in to YouTube. The light is green; ON-AIR is available." : "Signed out — the light is red. You can still record locally; arming ON-AIR arms intent, and Start turns that intent into a sign-in."; // 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"); _notifications.Info("Signed out of YouTube", "The saved session expired — sign in again when you next go live."); return; } } IsConnected = true; SyncConnectedAccount(); } catch (Exception ex) { AppLog.Write($"LoadSavedSessionAsync failed: {ex}"); } } private void SyncConnectedAccount() { var channel = _youtubeAuth.CurrentChannel; AccountAvatarUrl = channel?.ProfileImageUrl ?? string.Empty; AccountDisplayName = channel?.DisplayName ?? string.Empty; } // 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 SignInAsync() { try { var channel = await Task.Run(() => _youtubeAuth.AuthenticateAsync()); if (channel == null) { _notifications.Warning("Sign-in was unsuccessful", "Please try again."); return null; } IsConnected = true; SyncConnectedAccount(); return channel; } catch (Exception ex) { _notifications.Error("Sign-in failed", ex.Message); return null; } } /// Signs out of YouTube (the delete-the-YouTube-slot action in the dialog). private async Task SignOutYouTubeAsync() { _youtubeAuth.ClearSession(); TokenStore.Clear(); IsConnected = false; SyncConnectedAccount(); NotifySocialsChanged(); AppLog.Write("Socials: signed out of YouTube"); await Task.CompletedTask; } private async Task ChangeAccountAsync() { await SignOutYouTubeAsync(); await SignInAsync(); } }