feat(ui): Show Desktop toggle, avatar login, chat preview, backdrop seeding, border animations

- Source.ShowDesktop bool persisted in DB (schema migration + LayoutStore)
- ClearBackdropCaptureAsync clears CaptureKey + VideoImageSource for static fallback
- Top bar: red/green/pulsing status light, avatar via BitmapImage, Log In button
- ShowStartStream now requires IsConnected
- RunMockChatPreviewAsync: simple async loop, ChatPreviewEnabled property on Source
- EnsureDefaultBackdrop called on every scene switch, seeds all 5 scenes
- SeedEndingBackdrop replaces existing, SeedChatBackdrop added
- SceneCatalog: Chat scene display name corrected (layer = 'YouTube Chat')
- Left panel spacing consistency, context menu reordered
- Border animation rendering via code-behind Storyboard approach
- 3 new backdrop assets (ending-backdrop.jpg, chat-backdrop.jpg, live-backdrop.jpg)
- Build 0 warnings, 178 tests passing
This commit is contained in:
2026-08-20 18:02:41 -07:00
parent a76a3d2acd
commit 0bd17ab428
18 changed files with 1022 additions and 216 deletions
+99
View File
@@ -26,6 +26,49 @@ public sealed class Win32FullScreenDetector : IFullScreenDetector
private const int DwmwaExtendedFrameBounds = 9;
private const uint MonitorDefaultToNearest = 2;
private const uint MonitorInfoFPrimary = 0x00000001;
private const uint EventSystemForeground = 0x0003;
private const uint WINEVENTOutofContext = 0x0000;
private const int GWL_EXSTYLE = -20;
private IntPtr _hookHandle;
private WinEventProc? _hookProc;
private int? _lastReportedMonitor;
public event Action<int?>? FullscreenMonitorChanged;
public void StartWatching()
{
if (_hookHandle != IntPtr.Zero) return;
_hookProc = ForegroundHookCallback;
_hookHandle = SetWinEventHook(EventSystemForeground, EventSystemForeground,
IntPtr.Zero, _hookProc, 0, 0, WINEVENTOutofContext);
var existing = FindAnyFullscreenGameMonitor();
if (existing != null)
{
_lastReportedMonitor = existing;
FullscreenMonitorChanged?.Invoke(existing);
}
}
public void StopWatching()
{
if (_hookHandle == IntPtr.Zero) return;
UnhookWinEvent(_hookHandle);
_hookHandle = IntPtr.Zero;
_hookProc = null;
_lastReportedMonitor = null;
}
private void ForegroundHookCallback(IntPtr hWinEventHook, uint eventType,
IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
if (hwnd == IntPtr.Zero || IsOwnWindow(hwnd)) return;
var monitor = GetForegroundFullScreenMonitorIndex();
if (monitor == _lastReportedMonitor) return;
_lastReportedMonitor = monitor;
FullscreenMonitorChanged?.Invoke(monitor);
}
public int? GetForegroundFullScreenMonitorIndex()
{
@@ -69,6 +112,43 @@ public sealed class Win32FullScreenDetector : IFullScreenDetector
return index >= 0 ? index : null;
}
/// <summary>
/// Scans every visible top-level window (not just the foreground one) for
/// full-screen state. Used at startup to catch a game already running on
/// another monitor before the hook has a chance to fire.
/// </summary>
private int? FindAnyFullscreenGameMonitor()
{
int? found = null;
EnumWindows((hwnd, _) =>
{
if (found != null) return false;
if (hwnd == IntPtr.Zero || IsOwnWindow(hwnd)) return true;
if (!IsWindowVisible(hwnd)) return true;
if (!HasAppWindowStyle(hwnd)) return true;
if (!TryGetExtendedFrameBounds(hwnd, out var bounds)) return true;
var monitor = MonitorFromWindow(hwnd, MonitorDefaultToNearest);
if (monitor == IntPtr.Zero || !TryGetMonitorInfo(monitor, out var info)) return true;
var r = info.RcMonitor;
if (bounds.Left <= r.Left && bounds.Top <= r.Top &&
bounds.Right >= r.Right && bounds.Bottom >= r.Bottom)
found = MonitorIndex(monitor);
return found == null;
}, IntPtr.Zero);
return found;
}
private static bool HasAppWindowStyle(IntPtr hwnd)
{
const uint WS_EX_APPWINDOW = 0x00040000;
const uint WS_EX_TOOLWINDOW = 0x00000080;
var exStyle = (uint)GetWindowLongPtr(hwnd, GWL_EXSTYLE);
return (exStyle & WS_EX_TOOLWINDOW) == 0 &&
(exStyle & WS_EX_APPWINDOW) != 0;
}
public int PrimaryMonitorIndex()
=> GetDisplays().FirstOrDefault(d => d.IsPrimary)?.Index ?? 0;
@@ -191,4 +271,23 @@ public sealed class Win32FullScreenDetector : IFullScreenDetector
private static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, ref Win32Rect pvAttribute, int cbAttribute);
private delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref Win32Rect lprcMonitor, IntPtr dwData);
private delegate void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventProc pfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
[DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
private delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
[DllImport("user32.dll")]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(IntPtr hwnd);
[DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex);
}