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:
@@ -24,6 +24,7 @@ public sealed class ChatBoxRenderer
|
||||
int width,
|
||||
int height,
|
||||
double fontSize,
|
||||
string fontFamily,
|
||||
string fontColorHex,
|
||||
string backgroundColorHex,
|
||||
int maxMessages,
|
||||
@@ -79,7 +80,7 @@ public sealed class ChatBoxRenderer
|
||||
text,
|
||||
InvariantCulture,
|
||||
FlowDirection.LeftToRight,
|
||||
new Typeface(new FontFamily("Segoe UI"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
|
||||
new Typeface(new FontFamily(fontFamily), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
|
||||
fontSize,
|
||||
brush,
|
||||
dpi);
|
||||
|
||||
@@ -18,4 +18,17 @@ public interface IFullScreenDetector
|
||||
|
||||
/// <summary>All monitors in capture-key order (enumeration order).</summary>
|
||||
IReadOnlyList<DisplayInfo> GetDisplays();
|
||||
|
||||
/// <summary>
|
||||
/// Raised on the UI thread whenever the foreground window changes.
|
||||
/// Payload is the full-screen monitor index, or null when the new
|
||||
/// foreground window is not full-screen / is one of our own windows.
|
||||
/// </summary>
|
||||
event Action<int?>? FullscreenMonitorChanged;
|
||||
|
||||
/// <summary>Hook EVENT_SYSTEM_FOREGROUND so <see cref="FullscreenMonitorChanged"/> fires.</summary>
|
||||
void StartWatching();
|
||||
|
||||
/// <summary>Unhook the foreground listener.</summary>
|
||||
void StopWatching();
|
||||
}
|
||||
|
||||
+20
-11
@@ -261,6 +261,12 @@ public class LayoutStore : IDisposable
|
||||
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatSuperChatColor TEXT NOT NULL DEFAULT '#eab308';";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
if (!columns.Contains("ShowDesktop"))
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ShowDesktop INTEGER NOT NULL DEFAULT 0;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
// v3 → v4: WebcamSceneConfig gains RectWidth/RectHeight (the pre-Round rect,
|
||||
@@ -560,7 +566,7 @@ public class LayoutStore : IDisposable
|
||||
cmd.CommandText = """
|
||||
SELECT Id, SceneId, AssetId, Type, Name, IsEnabled,
|
||||
X, Y, Width, Height, Opacity, MonitorIndex, ClipShape, IsMirrored,
|
||||
IsBackdrop, CaptureKey,
|
||||
IsBackdrop, CaptureKey, ShowDesktop,
|
||||
ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages,
|
||||
ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor
|
||||
FROM Source ORDER BY SortOrder
|
||||
@@ -586,14 +592,15 @@ public class LayoutStore : IDisposable
|
||||
IsMirrored = reader.GetInt32(13) != 0,
|
||||
IsBackdrop = reader.GetInt32(14) != 0,
|
||||
CaptureKey = reader.IsDBNull(15) ? null : reader.GetString(15),
|
||||
ChatFontSize = reader.IsDBNull(16) ? 16 : reader.GetDouble(16),
|
||||
ChatFontColor = reader.IsDBNull(17) ? "#FFFFFF" : reader.GetString(17),
|
||||
ChatBackgroundColor = reader.IsDBNull(18) ? "#80000000" : reader.GetString(18),
|
||||
ChatMaxMessages = reader.IsDBNull(19) ? 5 : reader.GetInt32(19),
|
||||
ChatShowTimestamps = reader.IsDBNull(20) ? true : reader.GetInt32(20) != 0,
|
||||
ChatShowBadges = reader.IsDBNull(21) ? true : reader.GetInt32(21) != 0,
|
||||
ChatMemberColor = reader.IsDBNull(22) ? "#e94560" : reader.GetString(22),
|
||||
ChatSuperChatColor = reader.IsDBNull(23) ? "#eab308" : reader.GetString(23),
|
||||
ShowDesktop = reader.GetInt32(16) != 0,
|
||||
ChatFontSize = reader.IsDBNull(17) ? 16 : reader.GetDouble(17),
|
||||
ChatFontColor = reader.IsDBNull(18) ? "#FFFFFF" : reader.GetString(18),
|
||||
ChatBackgroundColor = reader.IsDBNull(19) ? "#80000000" : reader.GetString(19),
|
||||
ChatMaxMessages = reader.IsDBNull(20) ? 5 : reader.GetInt32(20),
|
||||
ChatShowTimestamps = reader.IsDBNull(21) ? true : reader.GetInt32(21) != 0,
|
||||
ChatShowBadges = reader.IsDBNull(22) ? true : reader.GetInt32(22) != 0,
|
||||
ChatMemberColor = reader.IsDBNull(23) ? "#e94560" : reader.GetString(23),
|
||||
ChatSuperChatColor = reader.IsDBNull(24) ? "#eab308" : reader.GetString(24),
|
||||
};
|
||||
if (!sourcesByScene.TryGetValue(sceneId, out var list))
|
||||
sourcesByScene[sceneId] = list = new List<Source>();
|
||||
@@ -784,12 +791,12 @@ public class LayoutStore : IDisposable
|
||||
cmd.CommandText = """
|
||||
INSERT INTO Source (Id, SceneId, AssetId, Type, Name, IsEnabled,
|
||||
X, Y, Width, Height, Opacity, MonitorIndex,
|
||||
ClipShape, IsMirrored, IsBackdrop, CaptureKey, SortOrder,
|
||||
ClipShape, IsMirrored, IsBackdrop, CaptureKey, ShowDesktop, SortOrder,
|
||||
ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages,
|
||||
ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor)
|
||||
VALUES ($id, $sceneId, $assetId, $type, $name, $isEnabled,
|
||||
$x, $y, $w, $h, $opacity, $monitor,
|
||||
$clip, $mirrored, $isBackdrop, $captureKey, $sort,
|
||||
$clip, $mirrored, $isBackdrop, $captureKey, $showDesktop, $sort,
|
||||
$chatFontSize, $chatFontColor, $chatBgColor, $chatMaxMsgs,
|
||||
$chatTimestamps, $chatBadges, $chatMemberColor, $chatSuperChatColor)
|
||||
""";
|
||||
@@ -810,6 +817,7 @@ public class LayoutStore : IDisposable
|
||||
var mirroredP = cmd.Parameters.Add("$mirrored", SqliteType.Integer);
|
||||
var isBackdropP = cmd.Parameters.Add("$isBackdrop", SqliteType.Integer);
|
||||
var captureKeyP = cmd.Parameters.Add("$captureKey", SqliteType.Text);
|
||||
var showDesktopP = cmd.Parameters.Add("$showDesktop", SqliteType.Integer);
|
||||
var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer);
|
||||
var chatFontSizeP = cmd.Parameters.Add("$chatFontSize", SqliteType.Real);
|
||||
var chatFontColorP = cmd.Parameters.Add("$chatFontColor", SqliteType.Text);
|
||||
@@ -842,6 +850,7 @@ public class LayoutStore : IDisposable
|
||||
mirroredP.Value = source.IsMirrored ? 1 : 0;
|
||||
isBackdropP.Value = source.IsBackdrop ? 1 : 0;
|
||||
captureKeyP.Value = (object?)source.CaptureKey ?? DBNull.Value;
|
||||
showDesktopP.Value = source.ShowDesktop ? 1 : 0;
|
||||
sortP.Value = sort++;
|
||||
chatFontSizeP.Value = source.ChatFontSize;
|
||||
chatFontColorP.Value = source.ChatFontColor;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user