using System; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using TeamsISO.App.WinUI.Services; namespace TeamsISO.App.WinUI.Views; public sealed partial class SettingsDrawer : UserControl { /// /// Raised when the operator clicks the close button or hits Esc. The host /// (MainWindow) handles the actual collapse animation since drawer /// hosting lives at that level — the drawer just signals intent. /// public event EventHandler? CloseRequested; public SettingsDrawer() { InitializeComponent(); BuildTabStrip(); SelectTab("Appearance"); } private string _currentTab = "Appearance"; /// Build the tab buttons imperatively so the parse doesn't /// trigger a SelectionChanged before the code-behind is ready. private void BuildTabStrip() { foreach (var (label, key) in new[] { ("Appearance", "Appearance"), ("Routing", "Routing"), ("Display", "Display"), ("Control", "Control"), ("Advanced", "Advanced"), }) { var btn = new Button { Content = label, Tag = key, Style = (Style)Application.Current.Resources["ButtonTertiary"], }; btn.Click += (s, _) => { if (s is Button b && b.Tag is string k) SelectTab(k); }; TabStrip.Children.Add(btn); } } private void SelectTab(string key) { _currentTab = key; foreach (var child in TabStrip.Children) { if (child is Button b) { var isActive = (b.Tag as string) == key; b.Foreground = (Microsoft.UI.Xaml.Media.SolidColorBrush)Application.Current.Resources[ isActive ? "AccentCyanText" : "FgSecondary"]; } } RebuildTabContent(key); } private void RebuildTabContent(string key) { TabContent.Children.Clear(); switch (key) { case "Appearance": BuildAppearanceTab(); break; case "Routing": BuildRoutingTab(); break; case "Display": BuildDisplayTab(); break; case "Control": BuildControlTab(); break; case "Advanced": BuildAdvancedTab(); break; } } private void BuildRoutingTab() { TabContent.Children.Add(SettingHeader("Routing")); TabContent.Children.Add(SettingRow("Framerate", "30 fps", "Target framerate the engine normalizes incoming NDI feeds to.")); TabContent.Children.Add(SettingRow("Resolution", "1920 x 1080", "Output resolution applied to every routed participant.")); TabContent.Children.Add(SettingRow("Aspect mode", "Letterbox", "How sources whose aspect ratio doesn't match the target are framed.")); TabContent.Children.Add(SettingRow("Audio routing", "Per-participant", "Embed each participant's audio in their own NDI source.")); TabContent.Children.Add(SettingNote("Settings wired to the engine in a follow-up commit.")); } private void BuildDisplayTab() { TabContent.Children.Add(SettingHeader("Display & participants")); TabContent.Children.Add(SettingRow("Hide local self", "Yes", "Filter the operator's own preview from the participants table.")); TabContent.Children.Add(SettingRow("Auto-disable on departure", "No", "Keep routing alive when a participant goes offline.")); TabContent.Children.Add(SettingRow("Participant sort", "Join order", "Default sort for the participants table.")); TabContent.Children.Add(SettingRow("Minimize to tray", "No", "Keep TeamsISO running in the system tray when the window is minimized.")); TabContent.Children.Add(SettingRow("Launch Teams on startup", "No", "Start Teams in the background when TeamsISO opens.")); TabContent.Children.Add(SettingRow("Auto-hide Teams windows", "No", "Hide every visible Teams window after launch.")); TabContent.Children.Add(SettingRow("Auto-record on call", "No", "Start recording every active ISO when Teams enters a call.")); } private void BuildControlTab() { TabContent.Children.Add(SettingHeader("External control surface")); TabContent.Children.Add(SettingRow("REST + WebSocket", "127.0.0.1:9755", "HTTP control surface for Companion / Stream Deck.")); TabContent.Children.Add(SettingRow("OSC bridge", "127.0.0.1:9000", "UDP OSC for TouchOSC / hardware surfaces.")); TabContent.Children.Add(SettingRow("LAN reachable", "No", "Bind the REST surface to all interfaces (warning: no auth).")); TabContent.Children.Add(SettingNote("Control surface protocol unchanged from the WPF host. See docs/CONTROL-SURFACE.md.")); } private void BuildAdvancedTab() { TabContent.Children.Add(SettingHeader("Advanced")); TabContent.Children.Add(SettingRow("Embed Teams window", "No", "Experimental SetParent reparent of Teams' main window.")); TabContent.Children.Add(SettingRow("Logs", "%LOCALAPPDATA%\\TeamsISO\\Logs", "Where rolling daily Serilog files write.")); TabContent.Children.Add(SettingRow("Recordings", "%USERPROFILE%\\Videos\\TeamsISO", "Default per-show recording directory.")); TabContent.Children.Add(SettingRow("Diagnostic bundle", "Export", "Zip logs + config + presets for a bug report.")); } private void OnCloseClick(object sender, RoutedEventArgs e) => CloseRequested?.Invoke(this, EventArgs.Empty); private void OnApplyClick(object sender, RoutedEventArgs e) { // Tabs already persist on change; Apply is here for affordance // consistency with the WPF host. Could surface a toast in the future. CloseRequested?.Invoke(this, EventArgs.Empty); } private void OnResetClick(object sender, RoutedEventArgs e) { // Defaults restoration plumbing follows in the view-model wiring // commit. For now, just clear the dirty hint. DirtyHint.Text = "Reset queued — apply or close to commit."; } /// /// Appearance tab — theme picker, accent palette peek. The theme picker /// is the only setting that affects the UI immediately (others apply on /// the engine layer once wired). Selection writes to UIPreferences and /// flips Window.Content.RequestedTheme synchronously. /// private void BuildAppearanceTab() { TabContent.Children.Add(SettingHeader("Appearance")); var themeLabel = new TextBlock { Style = (Style)Application.Current.Resources["TextBody"], Text = "Theme", Margin = new Thickness(0, 0, 0, 6), }; TabContent.Children.Add(themeLabel); var themeRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; foreach (var (label, value) in new[] { ("System", "System"), ("Dark", "Dark"), ("Light", "Light"), }) { var btn = new RadioButton { Content = label, Tag = value, GroupName = "Theme", IsChecked = ThemeManager.Current.PreferenceMatches(value), }; btn.Checked += (s, _) => { if (s is RadioButton rb && rb.Tag is string v) { ThemeManager.Current.Set(v); } }; themeRow.Children.Add(btn); } TabContent.Children.Add(themeRow); TabContent.Children.Add(SettingNote( "Dark is the default for the 1:50am operator scene; light is for daytime " + "production. System follows the Windows app-mode preference.")); TabContent.Children.Add(SettingDivider()); TabContent.Children.Add(SettingHeader("Accent peek")); var accentRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 12 }; accentRow.Children.Add(AccentSwatch("Cyan", "AccentCyanSurface")); accentRow.Children.Add(AccentSwatch("Coral", "AccentCoral")); accentRow.Children.Add(AccentSwatch("Live", "StatusLive")); accentRow.Children.Add(AccentSwatch("Warn", "StatusWarn")); TabContent.Children.Add(accentRow); TabContent.Children.Add(SettingNote( "These accents work in both themes. Cyan stays bright as a surface fill " + "(text on top is near-black regardless of theme). For inline text use, the " + "light palette substitutes a darker cyan automatically.")); } // ──────────────────── helpers ────────────────────────────────────────── private static TextBlock SettingHeader(string text) => new() { Style = (Style)Application.Current.Resources["TextHeading"], Text = text, Margin = new Thickness(0, 0, 0, 8), }; private static Grid SettingRow(string label, string value, string? tooltip = null) { var g = new Grid { Margin = new Thickness(0, 0, 0, 6) }; g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(180) }); g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var l = new TextBlock { Style = (Style)Application.Current.Resources["TextBody"], Text = label, VerticalAlignment = VerticalAlignment.Center, }; var v = new TextBlock { Style = (Style)Application.Current.Resources["TextSubtle"], Text = value, VerticalAlignment = VerticalAlignment.Center, }; if (tooltip is not null) { ToolTipService.SetToolTip(l, tooltip); } Grid.SetColumn(l, 0); Grid.SetColumn(v, 1); g.Children.Add(l); g.Children.Add(v); return g; } private static TextBlock SettingNote(string text) => new() { Style = (Style)Application.Current.Resources["TextCaption"], Text = text, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 4, 0, 12), }; private static Border SettingDivider() => new() { Height = 1, Background = (SolidColorBrush)Application.Current.Resources["BorderSubtle"], Margin = new Thickness(0, 12, 0, 12), }; private static Border AccentSwatch(string label, string brushKey) { var inner = new StackPanel { Orientation = Orientation.Vertical, Spacing = 4 }; var swatch = new Border { Width = 80, Height = 32, CornerRadius = new Microsoft.UI.Xaml.CornerRadius(6), Background = (SolidColorBrush)Application.Current.Resources[brushKey], }; var caption = new TextBlock { Style = (Style)Application.Current.Resources["TextCaption"], Text = label, HorizontalAlignment = HorizontalAlignment.Center, }; inner.Children.Add(swatch); inner.Children.Add(caption); return new Border { Child = inner }; } }