230 lines
10 KiB
C#
230 lines
10 KiB
C#
|
|
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
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// 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.
|
||
|
|
/// </summary>
|
||
|
|
public event EventHandler? CloseRequested;
|
||
|
|
|
||
|
|
public SettingsDrawer()
|
||
|
|
{
|
||
|
|
InitializeComponent();
|
||
|
|
BuildAppearanceTab();
|
||
|
|
}
|
||
|
|
|
||
|
|
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.";
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Tab-switch handler builds the body for the selected tab. We assemble
|
||
|
|
/// content imperatively rather than via separate XAML pages because the
|
||
|
|
/// drawer's content is data-driven (most rows are simple label + control
|
||
|
|
/// + tooltip triples) and pulling that into five XAML files would
|
||
|
|
/// triplicate the row layout. The body recomposes on every selection
|
||
|
|
/// change, which is cheap given the small surface.
|
||
|
|
/// </summary>
|
||
|
|
private void OnTabSelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||
|
|
{
|
||
|
|
TabContent.Children.Clear();
|
||
|
|
if (args.SelectedItemContainer is not NavigationViewItem item) return;
|
||
|
|
|
||
|
|
switch (item.Tag as string)
|
||
|
|
{
|
||
|
|
case "Appearance":
|
||
|
|
BuildAppearanceTab();
|
||
|
|
break;
|
||
|
|
case "Routing":
|
||
|
|
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."));
|
||
|
|
break;
|
||
|
|
case "Display":
|
||
|
|
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."));
|
||
|
|
break;
|
||
|
|
case "Control":
|
||
|
|
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."));
|
||
|
|
break;
|
||
|
|
case "Advanced":
|
||
|
|
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."));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 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.
|
||
|
|
/// </summary>
|
||
|
|
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 };
|
||
|
|
}
|
||
|
|
}
|