Some checks failed
CI / build-and-test (push) Failing after 27s
Four polish items + a test pass. 1. Inter Variable (rsms/inter v3.19, OFL) is bundled at Assets/Fonts/Inter.ttf (~800 KB) and registered as a WPF Resource. WildDragonTheme.xaml's Wd.Font.Sans now points at pack://application:,,,/Assets/Fonts/#Inter so the typography matches wilddragon.net regardless of whether the user has Inter installed system-wide. Falls back to Segoe UI Variable Display if the resource is missing. 2. 'Stop all ISOs' button at the right of the participants header. Bound to a new MainViewModel.StopAllIsosCommand that snapshots the enabled list, awaits DisableIsoAsync sequentially, and silently swallows per-pipeline failures (best-effort emergency stop). CanExecute gates on whether any ISO is currently enabled. 3. WindowStateStore service persists the main window's Left/Top/Width/Height/State to %LOCALAPPDATA%\\TeamsISO\\window.json on close and restores it on SourceInitialized. Multi-monitor friendly: a saved position with no corner inside any virtual screen is rejected so a disconnected monitor doesn't strand the window off-screen. 4. Two new unit tests cover FrameProcessor's drops + duplicates accounting. 76/76 unit tests pass (was 74).
116 lines
4.2 KiB
C#
116 lines
4.2 KiB
C#
using System.IO;
|
||
using System.Text.Json;
|
||
using System.Windows;
|
||
|
||
namespace TeamsISO.App.Services;
|
||
|
||
/// <summary>
|
||
/// Saves / restores the main window's size, position, and state across launches.
|
||
/// Stored as JSON at <c>%LOCALAPPDATA%\TeamsISO\window.json</c>. Multi-monitor
|
||
/// friendly: a saved position that no longer falls inside any working area is
|
||
/// rejected on restore so the window doesn't disappear off-screen when a monitor
|
||
/// has been disconnected.
|
||
/// </summary>
|
||
public static class WindowStateStore
|
||
{
|
||
private static readonly string Path =
|
||
System.IO.Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||
"TeamsISO",
|
||
"window.json");
|
||
|
||
public sealed record Snapshot(
|
||
double Left,
|
||
double Top,
|
||
double Width,
|
||
double Height,
|
||
WindowState State);
|
||
|
||
/// <summary>Save the current window placement.</summary>
|
||
public static void Save(Window window)
|
||
{
|
||
try
|
||
{
|
||
var snap = new Snapshot(
|
||
Left: window.Left,
|
||
Top: window.Top,
|
||
Width: window.ActualWidth,
|
||
Height: window.ActualHeight,
|
||
State: window.WindowState == WindowState.Minimized ? WindowState.Normal : window.WindowState);
|
||
var dir = System.IO.Path.GetDirectoryName(Path);
|
||
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
||
File.WriteAllText(Path, JsonSerializer.Serialize(snap, new JsonSerializerOptions { WriteIndented = true }));
|
||
}
|
||
catch
|
||
{
|
||
// Best-effort persistence; never crash on shutdown for a UI nicety.
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Apply a previously-saved placement. Clamps onto a visible work area so a
|
||
/// monitor change doesn't strand the window off-screen. Returns true if a
|
||
/// valid snapshot was applied; false if no file existed or the snapshot was
|
||
/// rejected for being entirely outside any visible work area.
|
||
/// </summary>
|
||
public static bool TryApply(Window window)
|
||
{
|
||
try
|
||
{
|
||
if (!File.Exists(Path)) return false;
|
||
var json = File.ReadAllText(Path);
|
||
var snap = JsonSerializer.Deserialize<Snapshot>(json);
|
||
if (snap is null) return false;
|
||
|
||
// Sanity-check sizes (don't restore a 0×0 or absurdly large window).
|
||
if (snap.Width < 320 || snap.Height < 240) return false;
|
||
if (snap.Width > 16000 || snap.Height > 12000) return false;
|
||
|
||
// Reject if entirely off-screen (any working area on any screen contains
|
||
// a corner). System.Windows.Forms gives us per-monitor work areas here;
|
||
// we deliberately stick with WPF's SystemParameters which only reports the
|
||
// primary, so we use a generous on-screen check rather than refusing
|
||
// multi-monitor positions.
|
||
if (!IsAnyCornerOnScreen(snap)) return false;
|
||
|
||
window.WindowStartupLocation = WindowStartupLocation.Manual;
|
||
window.Left = snap.Left;
|
||
window.Top = snap.Top;
|
||
window.Width = snap.Width;
|
||
window.Height = snap.Height;
|
||
window.WindowState = snap.State;
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Approximate "is at least one corner of the saved rect within the virtual
|
||
/// screen?" check. Uses SystemParameters.VirtualScreen* which spans every
|
||
/// monitor.
|
||
/// </summary>
|
||
private static bool IsAnyCornerOnScreen(Snapshot snap)
|
||
{
|
||
var minX = SystemParameters.VirtualScreenLeft;
|
||
var minY = SystemParameters.VirtualScreenTop;
|
||
var maxX = minX + SystemParameters.VirtualScreenWidth;
|
||
var maxY = minY + SystemParameters.VirtualScreenHeight;
|
||
|
||
var corners = new[]
|
||
{
|
||
(snap.Left, snap.Top),
|
||
(snap.Left + snap.Width, snap.Top),
|
||
(snap.Left, snap.Top + snap.Height),
|
||
(snap.Left + snap.Width, snap.Top + snap.Height),
|
||
};
|
||
foreach (var (x, y) in corners)
|
||
{
|
||
if (x >= minX && x <= maxX && y >= minY && y <= maxY)
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
}
|