diff --git a/src/TeamsISO.App/App.xaml.cs b/src/TeamsISO.App/App.xaml.cs index 25ca90c..fe0aa29 100644 --- a/src/TeamsISO.App/App.xaml.cs +++ b/src/TeamsISO.App/App.xaml.cs @@ -221,6 +221,42 @@ public partial class App : Application await _viewModel.InitializeAsync(CancellationToken.None); + // Auto-launch Teams in the background if the operator has opted in. + // Combined with AutoHideTeamsWindows this gives the "I only see + // TeamsISO" experience — Teams runs but never appears on screen, + // and all interaction routes through the IN-CALL bar + participants + // DataGrid. Fire-and-forget so a slow Teams launch doesn't delay + // TeamsISO's window from appearing. + if (_viewModel.Settings.LaunchTeamsOnStartup && !Services.TeamsLauncher.IsRunning()) + { + _ = Task.Run(() => + { + try + { + if (Services.TeamsLauncher.TryLaunch(out var launchError)) + { + if (_viewModel.Settings.AutoHideTeamsWindows) + _ = Services.TeamsLauncher.AutoHideAfterLaunchAsync(); + } + else + { + logger.LogWarning("Auto-launch Teams on startup failed: {Error}", launchError); + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Auto-launch Teams on startup threw"); + } + }); + } + else if (_viewModel.Settings.AutoHideTeamsWindows && Services.TeamsLauncher.IsRunning()) + { + // Teams is already up from a previous session. If auto-hide is + // on, hide it now so the operator's "I only see TeamsISO" rule + // applies even when Teams was launched externally. + _ = Services.TeamsLauncher.AutoHideAfterLaunchAsync(); + } + // Background update check, throttled to once per 24h. Fire-and-forget // so a slow / offline update server never delays startup. Surfaces a // banner via UpdateBanner if newer; failures just log. diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml index d6792d7..52eaf20 100644 --- a/src/TeamsISO.App/MainWindow.xaml +++ b/src/TeamsISO.App/MainWindow.xaml @@ -1251,6 +1251,20 @@ Margin="0,12,0,0" ToolTip="When checked, minimizing the window hides it and shows a tray icon. Useful for long unattended shows. Double-click the tray icon to restore."/> + + + + + + /// Fire-and-forget background watcher that polls every 250ms for up to + /// and hides any visible top-level Teams + /// windows it finds. Used after launch so the operator never sees the + /// Teams UI flash on screen — Teams takes 2-5s to splash + render its + /// main window, and the splash arrives separately from the main window + /// (so we keep polling past the first hide to catch follow-up windows). + /// + /// Returns the Task so callers can await completion if they want, but + /// production code should fire-and-forget. Exceptions are swallowed — + /// failure to hide is harmless (user just sees Teams briefly). + /// + public static Task AutoHideAfterLaunchAsync(TimeSpan? timeout = null, CancellationToken ct = default) + { + var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(15)); + return Task.Run(async () => + { + try + { + var hiddenAny = false; + while (!ct.IsCancellationRequested && DateTime.UtcNow < deadline) + { + // Poll for visible windows. Each iteration may catch new + // ones — Teams sometimes opens a small splash, then a + // larger main window 1-2s later, then a "What's new" + // banner. Keep hiding until we've gone a full second + // with nothing new appearing. + var hidden = HideWindows(); + if (hidden > 0) + { + hiddenAny = true; + // Settling delay: after we hide windows, wait a beat + // before polling again so we don't busy-loop while + // Teams' window manager catches up. + await Task.Delay(750, ct).ConfigureAwait(false); + } + else if (hiddenAny) + { + // We hid at least once; if the next poll finds + // nothing, Teams has settled. Bail early. + return; + } + else + { + // Teams hasn't materialized yet; keep waiting. + await Task.Delay(250, ct).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) { /* expected on cancel */ } + catch { /* defensive — auto-hide is best-effort, never breaks the app */ } + }, ct); + } + // ──────────────────────────────────────────────────────────────────── // Keyboard-shortcut forwarding (PostMessage path). // diff --git a/src/TeamsISO.App/Services/UIPreferences.cs b/src/TeamsISO.App/Services/UIPreferences.cs index 0861919..55b3b66 100644 --- a/src/TeamsISO.App/Services/UIPreferences.cs +++ b/src/TeamsISO.App/Services/UIPreferences.cs @@ -39,7 +39,13 @@ public static class UIPreferences bool AutoDisableOnDeparture = false, SortMode ParticipantSort = SortMode.JoinOrder, bool MinimizeToTray = false, - bool ControlSurfaceLanReachable = 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); public static Prefs Load() { diff --git a/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs b/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs index 000d23d..9d0f7f8 100644 --- a/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs +++ b/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs @@ -34,6 +34,8 @@ public sealed class GlobalSettingsViewModel : ObservableObject private UIPreferences.SortMode _participantSort = UIPreferences.SortMode.JoinOrder; private bool _minimizeToTray; private bool _controlSurfaceLanReachable; + private bool _launchTeamsOnStartup; + private bool _autoHideTeamsWindows; public GlobalSettingsViewModel(IIsoController controller, ToastViewModel? toast = null) { @@ -59,6 +61,8 @@ public sealed class GlobalSettingsViewModel : ObservableObject _participantSort = uiPrefs.ParticipantSort; _minimizeToTray = uiPrefs.MinimizeToTray; _controlSurfaceLanReachable = uiPrefs.ControlSurfaceLanReachable; + _launchTeamsOnStartup = uiPrefs.LaunchTeamsOnStartup; + _autoHideTeamsWindows = uiPrefs.AutoHideTeamsWindows; // Bring the auto-apply flag in from the presets store so the checkbox // reflects the user's prior choice when the settings panel opens. @@ -222,7 +226,41 @@ public sealed class GlobalSettingsViewModel : ObservableObject AutoDisableOnDeparture: _autoDisableOnDeparture, ParticipantSort: _participantSort, MinimizeToTray: _minimizeToTray, - ControlSurfaceLanReachable: _controlSurfaceLanReachable)); + ControlSurfaceLanReachable: _controlSurfaceLanReachable, + LaunchTeamsOnStartup: _launchTeamsOnStartup, + AutoHideTeamsWindows: _autoHideTeamsWindows)); + + /// + /// Auto-launch the Microsoft Teams desktop client when TeamsISO starts. + /// Paired with gives the operator a + /// "TeamsISO is the only window I see" experience — Teams runs in the + /// background, all interaction happens through the participants DataGrid + /// + IN-CALL bar. + /// + public bool LaunchTeamsOnStartup + { + get => _launchTeamsOnStartup; + set + { + if (SetField(ref _launchTeamsOnStartup, value)) PersistUiPrefs(); + } + } + + /// + /// Auto-hide Teams' top-level windows as soon as they materialize after + /// a launch (whether triggered via , + /// the rail button, or the eye-toggle). Runs a brief background poll + /// that calls TeamsLauncher.HideWindows every ~250ms for up to + /// 15 seconds, catching splash + main + follow-up panels. + /// + public bool AutoHideTeamsWindows + { + get => _autoHideTeamsWindows; + set + { + if (SetField(ref _autoHideTeamsWindows, value)) PersistUiPrefs(); + } + } /// /// Record each newly-enabled ISO's normalized output to disk under