dragon-iso/src/TeamsISO.App/Services/UIPreferences.cs

89 lines
3.6 KiB
C#
Raw Normal View History

using System.IO;
using System.Text.Json;
namespace TeamsISO.App.Services;
/// <summary>
/// Persistent UI-side toggles that don't belong in <see cref="TeamsISO.Engine.Persistence.ConfigStore"/>
/// (which is the engine's domain model — framerate, NDI groups, ISO assignments).
///
/// Each toggle is a property on a single record persisted as JSON at
/// <c>%LOCALAPPDATA%\TeamsISO\ui-prefs.json</c>. Defaults match the original
/// in-memory behavior: HideLocalSelf=true (filter the operator's own preview
/// out of the participants list) and AutoDisableOnDeparture=false (a participant
/// going offline doesn't tear down their pipeline by default — operators
/// usually want to keep the routing in case they reconnect).
///
/// Centralizing these here means the settings VM doesn't have to plumb
/// individual Set methods to dedicated services for every new bool.
/// </summary>
public static class UIPreferences
{
private static readonly object _gate = new();
private static string PrefsPath =>
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"TeamsISO", "ui-prefs.json");
/// <summary>
/// Sort modes for the participants DataGrid. <see cref="JoinOrder"/> is the default
/// and matches the engine's discovery order (operators with custom Stream Deck
/// layouts sometimes prefer Alphabetical for stability across meetings).
/// <see cref="LoudestFirst"/> resorts at the 1Hz stats tick so the active
/// speaker bubbles to the top — useful for operators reacting to who's talking.
/// </summary>
public enum SortMode { JoinOrder, Alphabetical, OnlineFirst, LoudestFirst }
/// <summary>The on-disk shape. New fields added here become opt-in for older files via default values.</summary>
public sealed record Prefs(
bool HideLocalSelf = true,
bool AutoDisableOnDeparture = false,
SortMode ParticipantSort = SortMode.JoinOrder,
bool MinimizeToTray = false,
bool ControlSurfaceLanReachable = false,
// Phase E.1 / E.2 quality-of-life. With both true, the operator launches
// TeamsISO and never sees the Teams UI — Teams auto-starts in the
// background and its windows are auto-hidden as soon as they materialize.
// All control happens via the IN-CALL bar + participants DataGrid.
bool LaunchTeamsOnStartup = false,
bool AutoHideTeamsWindows = false,
Phase E.4 experimental: SetParent-embed Teams window inside TeamsISO Reparents Teams' main top-level window into a TeamsISO-owned host via Win32 SetParent + window-style stripping. Operator gets Teams visually INSIDE TeamsISO instead of as a separate window — completes the 'Teams runs within this app' direction the user asked for after auto-hide. Strictly opt-in (DISPLAY tab → 'Embed Teams window (experimental)'). Modern Teams runs WebView2 in its main window; WebView2 is sensitive to parent changes and may render glitches or refuse focus. If so, operator unticks and falls back to auto-hide mode. Implementation: - TeamsLauncher.EmbedTeamsInto(hostHwnd, w, h): finds Teams' main window (longest-title heuristic — same as GetActiveWindowTitle), saves original parent + WS_STYLE, SetParents into host, strips WS_CAPTION + WS_THICKFRAME + WS_BORDER + WS_DLGFRAME + WS_POPUP, adds WS_CHILD, MoveWindow to fit. - TeamsLauncher.RestoreEmbed(): SetParent back to desktop + restore saved window styles. Idempotent — safe to call on shutdown even if nothing was embedded. - TeamsLauncher.ResizeEmbedded(w, h): MoveWindow to new dimensions; called from host SizeChanged event. - New TeamsEmbedWindow chromeless host with an EXPERIMENTAL pill in the caption. Loaded → grab HwndSource from EmbedHost Border → call EmbedTeamsInto. SizeChanged → ResizeEmbedded. Closed → RestoreEmbed (in try/finally so a crash can't leave Teams orphaned). Friendly fallback messages if no Teams window exists or HWND grab fails. - Settings → DISPLAY → checkbox + 'Open embed window' button (gated by the checkbox). Persisted via EmbedTeamsWindow on UIPreferences.
2026-05-10 21:14:42 -04:00
// Experimental Phase E.4. SetParent-reparents Teams' main window
// into a TeamsISO-owned host. WebView2 in modern Teams can render
// weirdly after reparent; if so the operator unticks and falls
// back to auto-hide mode. Off by default.
bool EmbedTeamsWindow = false);
public static Prefs Load()
{
try
{
if (!File.Exists(PrefsPath)) return new Prefs();
var json = File.ReadAllText(PrefsPath);
return JsonSerializer.Deserialize<Prefs>(json) ?? new Prefs();
}
catch
{
return new Prefs();
}
}
public static void Save(Prefs prefs)
{
try
{
lock (_gate)
{
var dir = Path.GetDirectoryName(PrefsPath);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
var json = JsonSerializer.Serialize(prefs, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(PrefsPath, json);
}
}
catch
{
// Disk full / permission denied — in-memory state still holds for this session.
}
}
}