diff --git a/src/TeamsISO.App/Assets/Fonts/Inter.ttf b/src/TeamsISO.App/Assets/Fonts/Inter.ttf
new file mode 100644
index 0000000..1cb674b
Binary files /dev/null and b/src/TeamsISO.App/Assets/Fonts/Inter.ttf differ
diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml
index d589741..e8fd153 100644
--- a/src/TeamsISO.App/MainWindow.xaml
+++ b/src/TeamsISO.App/MainWindow.xaml
@@ -304,18 +304,43 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /// Restore the window's previous placement after the HWND is created (so
+ /// SetWindowPos / WindowState transitions actually take effect). Falls
+ /// silently back to the XAML-default startup location if no snapshot exists.
+ ///
+ private void OnSourceInitialized(object? sender, EventArgs e)
+ {
+ WindowStateStore.TryApply(this);
+ }
+
+ /// Persist the placement on close so next launch lands in the same spot.
+ private void OnClosing(object? sender, System.ComponentModel.CancelEventArgs e)
+ {
+ WindowStateStore.Save(this);
+ }
+
/// Custom min button — chrome'd window has no system caption buttons.
private void OnMinimize(object sender, RoutedEventArgs e) =>
WindowState = WindowState.Minimized;
diff --git a/src/TeamsISO.App/Services/WindowStateStore.cs b/src/TeamsISO.App/Services/WindowStateStore.cs
new file mode 100644
index 0000000..dec4ecd
--- /dev/null
+++ b/src/TeamsISO.App/Services/WindowStateStore.cs
@@ -0,0 +1,116 @@
+using System.IO;
+using System.Text.Json;
+using System.Windows;
+
+namespace TeamsISO.App.Services;
+
+///
+/// Saves / restores the main window's size, position, and state across launches.
+/// Stored as JSON at %LOCALAPPDATA%\TeamsISO\window.json. 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.
+///
+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);
+
+ /// Save the current window placement.
+ 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.
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ public static bool TryApply(Window window)
+ {
+ try
+ {
+ if (!File.Exists(Path)) return false;
+ var json = File.ReadAllText(Path);
+ var snap = JsonSerializer.Deserialize(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;
+ }
+ }
+
+ ///
+ /// Approximate "is at least one corner of the saved rect within the virtual
+ /// screen?" check. Uses SystemParameters.VirtualScreen* which spans every
+ /// monitor.
+ ///
+ 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;
+ }
+}
diff --git a/src/TeamsISO.App/TeamsISO.App.csproj b/src/TeamsISO.App/TeamsISO.App.csproj
index 9b9eab7..62e68d9 100644
--- a/src/TeamsISO.App/TeamsISO.App.csproj
+++ b/src/TeamsISO.App/TeamsISO.App.csproj
@@ -20,6 +20,11 @@
+
+
diff --git a/src/TeamsISO.App/Themes/WildDragonTheme.xaml b/src/TeamsISO.App/Themes/WildDragonTheme.xaml
index d29ef83..9282d82 100644
--- a/src/TeamsISO.App/Themes/WildDragonTheme.xaml
+++ b/src/TeamsISO.App/Themes/WildDragonTheme.xaml
@@ -69,7 +69,13 @@
- Inter, Segoe UI Variable Display, Segoe UI, sans-serif
+
+ pack://application:,,,/Assets/Fonts/#Inter, Inter, Segoe UI Variable Display, Segoe UI, sans-serif
JetBrains Mono, Cascadia Mono, Consolas, monospace