feat(winui3): ThemeManager service + Settings drawer + Help/About/Onboarding
Builds out the secondary surfaces of the redesigned WinUI 3 host.
ThemeManager (Services/ThemeManager.cs)
Single-source-of-truth for the active theme. Holds the user preference
(System / Dark / Light), resolves it to ElementTheme at request, and
raises a Themed event when it changes so the MainWindow can push the
AppWindow title-bar button colors. Uses Windows.UI.ViewManagement
UISettings to follow the OS app-mode when preference is System.
Persistence to UIPreferences lands in the engine-wiring commit.
MainWindow theme wiring
Replaces the per-handler theme toggle with a ThemeManager subscription:
click the title-bar sun/moon -> Toggle() -> Themed event ->
ApplyResolvedTheme on the visual tree + the title-bar buttons. Glyph
cue: sun = "current is Light, click to Dark"; moon = "current is Dark,
click to Light." Initial state applied at construction so the first
frame matches the preference.
SettingsDrawer (Views/SettingsDrawer.xaml + .cs)
UserControl that slides in from the right over the participants table.
56px header, NavigationView with five tabs (Appearance, Routing,
Display, Control, Advanced), footer with Reset-to-defaults +
Apply/Close. Appearance tab has the theme tri-state picker (System /
Dark / Light radio group) and an "Accent peek" row showing the four
brand accents (cyan / coral / live / warn) as swatches so the
operator can verify Wild Dragon brand is respected on a light desk.
CloseRequested event signals the host to collapse the drawer.
HelpDialog (Views/HelpDialog.xaml + .cs)
ContentDialog with the keyboard shortcut cheat sheet, grouped by
category (Global / Participants / Look / Control surface). 540px max
height with scroll, mono-spaced shortcut labels at left, body text at
right. Replaces the WPF host's HelpWindow at parity.
AboutDialog (Views/AboutDialog.xaml + .cs)
ContentDialog with the Wild Dragon mark, version + host + engine +
brand info as label/value rows, and three quick action buttons
(open logs folder, open recordings, check for updates). Mirrors the
WPF host's AboutWindow.
OnboardingDialog (Views/OnboardingDialog.xaml + .cs)
Three numbered steps (Install NDI Runtime / Enable Teams NDI / Pick
transcoder topology), no carousel, operator-tone copy ("Don't show
this again" defaults checked). PrimaryButtonText "Get started",
SecondaryButtonText "Skip" so the dialog is skippable from the first
frame as the PRODUCT.md anti-references demand.
Build clean: dotnet build TeamsISO.App.WinUI -c Debug -> 0 / 0.
Next: wire the drawer's CloseRequested into MainWindow (so the settings
icon actually opens / collapses the drawer), then attack the runtime
activation blocker (Phase 3 of the migration plan).
This commit is contained in:
parent
2e6d2a1e5e
commit
48ca16bc5e
10 changed files with 797 additions and 35 deletions
108
src/TeamsISO.App.WinUI/Services/ThemeManager.cs
Normal file
108
src/TeamsISO.App.WinUI/Services/ThemeManager.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.UI;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Windows.UI;
|
||||||
|
using Windows.UI.ViewManagement;
|
||||||
|
|
||||||
|
namespace TeamsISO.App.WinUI.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the active theme for the WinUI 3 host. Three preferences:
|
||||||
|
/// <c>System</c> follows the Windows app-mode setting (default for new
|
||||||
|
/// users); <c>Dark</c> and <c>Light</c> pin one regardless of the OS choice.
|
||||||
|
/// The persistence path will land alongside the existing UIPreferences in
|
||||||
|
/// the next commit — for now state lives in-process.
|
||||||
|
///
|
||||||
|
/// All public mutations push <see cref="Themed"/> to subscribers so the
|
||||||
|
/// host (MainWindow) can update the AppWindow title-bar button colors
|
||||||
|
/// (system buttons aren't part of the visual tree and need a separate
|
||||||
|
/// poke when ElementTheme changes).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ThemeManager
|
||||||
|
{
|
||||||
|
public static ThemeManager Current { get; } = new();
|
||||||
|
|
||||||
|
private ThemeManager()
|
||||||
|
{
|
||||||
|
_uiSettings = new UISettings();
|
||||||
|
_uiSettings.ColorValuesChanged += OnSystemColorsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly UISettings _uiSettings;
|
||||||
|
private string _preference = "System";
|
||||||
|
|
||||||
|
public string Preference => _preference;
|
||||||
|
|
||||||
|
public event EventHandler<ElementTheme>? Themed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolve the preference to an absolute <see cref="ElementTheme"/>
|
||||||
|
/// suitable for <see cref="FrameworkElement.RequestedTheme"/>.
|
||||||
|
/// <c>System</c> resolves to the OS app-mode.
|
||||||
|
/// </summary>
|
||||||
|
public ElementTheme ResolveTheme() => _preference switch
|
||||||
|
{
|
||||||
|
"Dark" => ElementTheme.Dark,
|
||||||
|
"Light" => ElementTheme.Light,
|
||||||
|
_ => IsSystemDark() ? ElementTheme.Dark : ElementTheme.Light,
|
||||||
|
};
|
||||||
|
|
||||||
|
public bool PreferenceMatches(string value) => string.Equals(_preference, value, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cycle dark ↔ light from the title-bar toggle. If the current
|
||||||
|
/// preference is <c>System</c>, the cycle pins to the opposite of the
|
||||||
|
/// currently-resolved theme so the click has a visible effect.
|
||||||
|
/// </summary>
|
||||||
|
public ElementTheme Toggle()
|
||||||
|
{
|
||||||
|
var current = ResolveTheme();
|
||||||
|
Set(current == ElementTheme.Dark ? "Light" : "Dark");
|
||||||
|
return ResolveTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Set the preference and broadcast the resolved theme.</summary>
|
||||||
|
public void Set(string preference)
|
||||||
|
{
|
||||||
|
if (preference != "System" && preference != "Dark" && preference != "Light")
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Preference must be System, Dark, or Light.", nameof(preference));
|
||||||
|
}
|
||||||
|
|
||||||
|
_preference = preference;
|
||||||
|
Themed?.Invoke(this, ResolveTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsSystemDark()
|
||||||
|
{
|
||||||
|
// UISettings.GetColorValue(UIColorType.Background) returns
|
||||||
|
// black-ish in dark mode, white-ish in light mode — the most
|
||||||
|
// reliable cross-version check for app mode on desktop WinUI 3.
|
||||||
|
var bg = _uiSettings.GetColorValue(UIColorType.Background);
|
||||||
|
return ((5 * bg.G) + (2 * bg.R) + bg.B) < 8 * 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSystemColorsChanged(UISettings sender, object args)
|
||||||
|
{
|
||||||
|
// Only re-broadcast if the operator hasn't pinned a preference —
|
||||||
|
// otherwise the explicit choice wins regardless of what the OS does.
|
||||||
|
if (_preference == "System")
|
||||||
|
{
|
||||||
|
Themed?.Invoke(this, ResolveTheme());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compute the AppWindow title-bar foreground for the given resolved
|
||||||
|
/// theme so the system min/max/close buttons stay readable.
|
||||||
|
/// </summary>
|
||||||
|
public static Color TitleBarForegroundFor(ElementTheme theme) =>
|
||||||
|
theme == ElementTheme.Dark
|
||||||
|
? Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6)
|
||||||
|
: Color.FromArgb(0xFF, 0x0A, 0x0A, 0x0A);
|
||||||
|
|
||||||
|
public static Color TitleBarHoverBgFor(ElementTheme theme) =>
|
||||||
|
theme == ElementTheme.Dark
|
||||||
|
? Color.FromArgb(0xFF, 0x33, 0x34, 0x3A)
|
||||||
|
: Color.FromArgb(0xFF, 0xEC, 0xEE, 0xF1);
|
||||||
|
}
|
||||||
75
src/TeamsISO.App.WinUI/Views/AboutDialog.xaml
Normal file
75
src/TeamsISO.App.WinUI/Views/AboutDialog.xaml
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ContentDialog
|
||||||
|
x:Class="TeamsISO.App.WinUI.Views.AboutDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="About TeamsISO"
|
||||||
|
PrimaryButtonText="Close"
|
||||||
|
DefaultButton="Primary"
|
||||||
|
Background="{ThemeResource BgElevated}"
|
||||||
|
BorderBrush="{ThemeResource BorderStrong}">
|
||||||
|
|
||||||
|
<StackPanel Spacing="14" MinWidth="380">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="14">
|
||||||
|
<Border Width="56" Height="56"
|
||||||
|
CornerRadius="{ThemeResource RadiusM}"
|
||||||
|
Background="{ThemeResource AccentCyanMuted}">
|
||||||
|
<TextBlock Text="W"
|
||||||
|
FontFamily="{ThemeResource FontSans}"
|
||||||
|
FontSize="28"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="{ThemeResource AccentCyanText}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2">
|
||||||
|
<TextBlock Text="TeamsISO" Style="{StaticResource TextTitle}"/>
|
||||||
|
<TextBlock Text="Per-participant NDI ISO controller for Microsoft Teams"
|
||||||
|
Style="{StaticResource TextSubtle}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
MaxWidth="280"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Height="1" Background="{ThemeResource BorderSubtle}"/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="120"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Version" Style="{StaticResource TextSubtle}"/>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="1" Text="1.0.0-alpha" Style="{StaticResource TextMono}"/>
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Host" Style="{StaticResource TextSubtle}" Margin="0,6,0,0"/>
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="1" Text="WinUI 3 (WindowsAppSDK 1.6)" Style="{StaticResource TextMono}" Margin="0,6,0,0"/>
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="Engine" Style="{StaticResource TextSubtle}" Margin="0,6,0,0"/>
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="1" Text=".NET 8 + NDI 5" Style="{StaticResource TextMono}" Margin="0,6,0,0"/>
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="Brand" Style="{StaticResource TextSubtle}" Margin="0,6,0,0"/>
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="1" Text="Wild Dragon · wilddragon.net" Style="{StaticResource TextMono}" Margin="0,6,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Height="1" Background="{ThemeResource BorderSubtle}"/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button Style="{StaticResource ButtonSecondary}"
|
||||||
|
Content="Open logs folder"
|
||||||
|
ToolTipService.ToolTip="%LOCALAPPDATA%\TeamsISO\Logs"/>
|
||||||
|
<Button Style="{StaticResource ButtonSecondary}"
|
||||||
|
Content="Open recordings"
|
||||||
|
ToolTipService.ToolTip="%USERPROFILE%\Videos\TeamsISO"/>
|
||||||
|
<Button Style="{StaticResource ButtonSecondary}"
|
||||||
|
Content="Check for updates"
|
||||||
|
ToolTipService.ToolTip="Query forge.wilddragon.net for a newer release"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource TextCaption}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="Proprietary © Wild Dragon LLC 2026."/>
|
||||||
|
</StackPanel>
|
||||||
|
</ContentDialog>
|
||||||
11
src/TeamsISO.App.WinUI/Views/AboutDialog.xaml.cs
Normal file
11
src/TeamsISO.App.WinUI/Views/AboutDialog.xaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace TeamsISO.App.WinUI.Views;
|
||||||
|
|
||||||
|
public sealed partial class AboutDialog : ContentDialog
|
||||||
|
{
|
||||||
|
public AboutDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/TeamsISO.App.WinUI/Views/HelpDialog.xaml
Normal file
110
src/TeamsISO.App.WinUI/Views/HelpDialog.xaml
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ContentDialog
|
||||||
|
x:Class="TeamsISO.App.WinUI.Views.HelpDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Keyboard shortcuts"
|
||||||
|
PrimaryButtonText="Close"
|
||||||
|
DefaultButton="Primary"
|
||||||
|
Background="{ThemeResource BgElevated}"
|
||||||
|
BorderBrush="{ThemeResource BorderStrong}">
|
||||||
|
|
||||||
|
<ScrollViewer MaxHeight="540" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Spacing="6" MinWidth="420">
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource TextCaption}"
|
||||||
|
Text="GLOBAL"
|
||||||
|
Margin="0,4,0,4"/>
|
||||||
|
<Grid Padding="0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0" Text="F1" Style="{StaticResource TextMono}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Open this help dialog" Style="{StaticResource TextBody}"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Padding="0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0" Text="Ctrl + K" Style="{StaticResource TextMono}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Open the command palette" Style="{StaticResource TextBody}"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Padding="0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0" Text="Ctrl + Shift + S" Style="{StaticResource TextMono}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Stop every running ISO (panic)" Style="{StaticResource TextBody}"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Padding="0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0" Text="Ctrl + R" Style="{StaticResource TextMono}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Refresh NDI discovery" Style="{StaticResource TextBody}"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Padding="0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0" Text="Ctrl + M" Style="{StaticResource TextMono}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Drop a recording marker" Style="{StaticResource TextBody}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource TextCaption}"
|
||||||
|
Text="PARTICIPANTS"
|
||||||
|
Margin="0,16,0,4"/>
|
||||||
|
<Grid Padding="0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0" Text="NumPad 1-9 / 1-9" Style="{StaticResource TextMono}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Toggle ISO for the Nth visible participant" Style="{StaticResource TextBody}"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Padding="0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0" Text="Right-click row" Style="{StaticResource TextMono}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Open preview, rename output, restart pipeline" Style="{StaticResource TextBody}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource TextCaption}"
|
||||||
|
Text="LOOK"
|
||||||
|
Margin="0,16,0,4"/>
|
||||||
|
<Grid Padding="0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0" Text="Theme toggle" Style="{StaticResource TextMono}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Title-bar sun/moon icon swaps dark / light" Style="{StaticResource TextBody}"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid Padding="0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0" Text="Active speaker" Style="{StaticResource TextMono}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Row highlights with a cyan left border" Style="{StaticResource TextBody}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource TextCaption}"
|
||||||
|
Text="CONTROL SURFACE"
|
||||||
|
Margin="0,16,0,4"/>
|
||||||
|
<TextBlock Style="{StaticResource TextBody}"
|
||||||
|
TextWrapping="Wrap">
|
||||||
|
External control via REST + WebSocket on 127.0.0.1:9755 and OSC on UDP 127.0.0.1:9000.
|
||||||
|
Self-contained HTML panel at /ui. Use Bitfocus Companion or TouchOSC. See
|
||||||
|
docs/CONTROL-SURFACE.md for the command vocabulary.
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ContentDialog>
|
||||||
11
src/TeamsISO.App.WinUI/Views/HelpDialog.xaml.cs
Normal file
11
src/TeamsISO.App.WinUI/Views/HelpDialog.xaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace TeamsISO.App.WinUI.Views;
|
||||||
|
|
||||||
|
public sealed partial class HelpDialog : ContentDialog
|
||||||
|
{
|
||||||
|
public HelpDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ using Microsoft.UI;
|
||||||
using Microsoft.UI.Windowing;
|
using Microsoft.UI.Windowing;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using TeamsISO.App.WinUI.Models;
|
using TeamsISO.App.WinUI.Models;
|
||||||
|
using TeamsISO.App.WinUI.Services;
|
||||||
using Windows.Graphics;
|
using Windows.Graphics;
|
||||||
using Windows.UI;
|
using Windows.UI;
|
||||||
|
|
||||||
|
|
@ -28,9 +29,6 @@ public sealed partial class MainWindow : Window
|
||||||
|
|
||||||
AppWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent;
|
AppWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent;
|
||||||
AppWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
|
AppWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
|
||||||
AppWindow.TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(0xFF, 0x33, 0x34, 0x3A);
|
|
||||||
AppWindow.TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(0xFF, 0x3F, 0x3F, 0x47);
|
|
||||||
AppWindow.TitleBar.ButtonForegroundColor = Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6);
|
|
||||||
AppWindow.TitleBar.ButtonHoverForegroundColor = Colors.White;
|
AppWindow.TitleBar.ButtonHoverForegroundColor = Colors.White;
|
||||||
|
|
||||||
// ── Initial size & position ───────────────────────────────────────
|
// ── Initial size & position ───────────────────────────────────────
|
||||||
|
|
@ -44,46 +42,45 @@ public sealed partial class MainWindow : Window
|
||||||
// table is populated from a static sample list so the visual design
|
// table is populated from a static sample list so the visual design
|
||||||
// can be validated end-to-end against representative data.
|
// can be validated end-to-end against representative data.
|
||||||
ParticipantsRepeater.ItemsSource = MockParticipant.Sample();
|
ParticipantsRepeater.ItemsSource = MockParticipant.Sample();
|
||||||
|
|
||||||
|
// ── Theme system ──────────────────────────────────────────────────
|
||||||
|
// Subscribe to ThemeManager so picker changes from anywhere
|
||||||
|
// (settings drawer, title-bar toggle, system color change) reach
|
||||||
|
// the title-bar buttons and the visual tree consistently. Apply
|
||||||
|
// once at construction so the initial state matches the preference
|
||||||
|
// before the first frame.
|
||||||
|
ThemeManager.Current.Themed += (_, theme) => ApplyResolvedTheme(theme);
|
||||||
|
ApplyResolvedTheme(ThemeManager.Current.ResolveTheme());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cycle the active theme between Dark and Light. The actual swap target
|
/// Cycle the active theme between Dark and Light from the title-bar
|
||||||
/// is the window's root visual element — RequestedTheme on that element
|
/// toggle. The actual swap lives in <see cref="ThemeManager"/>; this
|
||||||
/// propagates the {ThemeResource} swap across the whole tree, with no
|
/// handler just calls Toggle() and lets the subscription propagate.
|
||||||
/// flicker. The system title-bar buttons aren't part of the visual tree,
|
|
||||||
/// so their colors are recomputed inline.
|
|
||||||
///
|
|
||||||
/// Persistence to UIPreferences.Theme happens in the view-model wiring
|
|
||||||
/// commit (the WinUI host doesn't yet share UIPreferences with the WPF
|
|
||||||
/// host; both will once the WinUI host owns the .NET startup).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnThemeToggleClick(object sender, RoutedEventArgs e)
|
private void OnThemeToggleClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ThemeManager.Current.Toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Push a resolved theme to the visual tree and to the AppWindow
|
||||||
|
/// title-bar buttons. Called on every <see cref="ThemeManager.Themed"/>
|
||||||
|
/// event and once at construction.
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyResolvedTheme(ElementTheme theme)
|
||||||
{
|
{
|
||||||
if (Content is FrameworkElement root)
|
if (Content is FrameworkElement root)
|
||||||
{
|
{
|
||||||
var next = root.ActualTheme == ElementTheme.Dark
|
root.RequestedTheme = theme;
|
||||||
? ElementTheme.Light
|
|
||||||
: ElementTheme.Dark;
|
|
||||||
root.RequestedTheme = next;
|
|
||||||
|
|
||||||
// Title-bar system buttons read from AppWindow.TitleBar directly,
|
|
||||||
// not from the XAML resource dictionary, so we set them inline
|
|
||||||
// for both themes. Dark-mode chrome lands on near-white glyphs;
|
|
||||||
// light-mode chrome lands on near-black glyphs.
|
|
||||||
if (next == ElementTheme.Dark)
|
|
||||||
{
|
|
||||||
AppWindow.TitleBar.ButtonForegroundColor = Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6);
|
|
||||||
AppWindow.TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(0xFF, 0x33, 0x34, 0x3A);
|
|
||||||
AppWindow.TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(0xFF, 0x3F, 0x3F, 0x47);
|
|
||||||
ThemeToggleIcon.Glyph = ""; // sun
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AppWindow.TitleBar.ButtonForegroundColor = Color.FromArgb(0xFF, 0x0A, 0x0A, 0x0A);
|
|
||||||
AppWindow.TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(0xFF, 0xEC, 0xEE, 0xF1);
|
|
||||||
AppWindow.TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(0xFF, 0xE0, 0xE3, 0xE7);
|
|
||||||
ThemeToggleIcon.Glyph = ""; // moon
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppWindow.TitleBar.ButtonForegroundColor = ThemeManager.TitleBarForegroundFor(theme);
|
||||||
|
AppWindow.TitleBar.ButtonHoverBackgroundColor = ThemeManager.TitleBarHoverBgFor(theme);
|
||||||
|
AppWindow.TitleBar.ButtonPressedBackgroundColor = ThemeManager.TitleBarHoverBgFor(theme);
|
||||||
|
|
||||||
|
// Glyph cue: sun () means current is Light, click moves to Dark;
|
||||||
|
// moon () means current is Dark, click moves to Light.
|
||||||
|
ThemeToggleIcon.Glyph = theme == ElementTheme.Light ? "" : "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
104
src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml
Normal file
104
src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ContentDialog
|
||||||
|
x:Class="TeamsISO.App.WinUI.Views.OnboardingDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Welcome to TeamsISO"
|
||||||
|
PrimaryButtonText="Get started"
|
||||||
|
SecondaryButtonText="Skip"
|
||||||
|
DefaultButton="Primary"
|
||||||
|
Background="{ThemeResource BgElevated}"
|
||||||
|
BorderBrush="{ThemeResource BorderStrong}">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
First-launch only. Three sections, one pane deep — no carousel,
|
||||||
|
no celebration. Operator-tone copy ("Pick your NDI groups" not
|
||||||
|
"Welcome to TeamsISO!"). Skippable from the first frame.
|
||||||
|
|
||||||
|
Suppressed after dismissal via UIPreferences (Phase 7).
|
||||||
|
-->
|
||||||
|
|
||||||
|
<StackPanel Spacing="20" MinWidth="500" MaxWidth="540">
|
||||||
|
<TextBlock Style="{StaticResource TextSubtle}" TextWrapping="Wrap">
|
||||||
|
TeamsISO sits between Microsoft Teams' NDI broadcast and your live-production switcher.
|
||||||
|
One-time setup gets you to the participants table.
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||||
|
<Border Width="28" Height="28"
|
||||||
|
CornerRadius="14"
|
||||||
|
Background="{ThemeResource AccentCyanMuted}">
|
||||||
|
<TextBlock Text="1"
|
||||||
|
Style="{StaticResource TextBody}"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{ThemeResource AccentCyanText}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="Install the NDI Runtime"
|
||||||
|
Style="{StaticResource TextHeading}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Style="{StaticResource TextSubtle}"
|
||||||
|
Margin="38,0,0,0"
|
||||||
|
TextWrapping="Wrap">
|
||||||
|
From https://ndi.video/tools/. TeamsISO won't start without it — the engine relies on
|
||||||
|
NDI 5 for discovery and routing. If the runtime is missing, you'll see a launch error;
|
||||||
|
install it then relaunch.
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||||
|
<Border Width="28" Height="28"
|
||||||
|
CornerRadius="14"
|
||||||
|
Background="{ThemeResource AccentCyanMuted}">
|
||||||
|
<TextBlock Text="2"
|
||||||
|
Style="{StaticResource TextBody}"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{ThemeResource AccentCyanText}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="Enable Teams NDI broadcast"
|
||||||
|
Style="{StaticResource TextHeading}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Style="{StaticResource TextSubtle}"
|
||||||
|
Margin="38,0,0,0"
|
||||||
|
TextWrapping="Wrap">
|
||||||
|
Teams admin must enable NDI broadcast for your tenant. In Teams, Settings → Devices →
|
||||||
|
"Allow NDI usage." Per-participant streams will appear as TeamsISO discovers them.
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||||
|
<Border Width="28" Height="28"
|
||||||
|
CornerRadius="14"
|
||||||
|
Background="{ThemeResource AccentCyanMuted}">
|
||||||
|
<TextBlock Text="3"
|
||||||
|
Style="{StaticResource TextBody}"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{ThemeResource AccentCyanText}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="Pick your transcoder topology"
|
||||||
|
Style="{StaticResource TextHeading}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Style="{StaticResource TextSubtle}"
|
||||||
|
Margin="38,0,0,0"
|
||||||
|
TextWrapping="Wrap">
|
||||||
|
Defaults to 1920×1080 at 30 fps with letterbox aspect. Adjust under Settings → Routing.
|
||||||
|
Recording outputs land in %USERPROFILE%\Videos\TeamsISO\<date>\.
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<CheckBox x:Name="DontShowAgain"
|
||||||
|
Content="Don't show this again"
|
||||||
|
IsChecked="True"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ContentDialog>
|
||||||
18
src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml.cs
Normal file
18
src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace TeamsISO.App.WinUI.Views;
|
||||||
|
|
||||||
|
public sealed partial class OnboardingDialog : ContentDialog
|
||||||
|
{
|
||||||
|
public OnboardingDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when the user wants the dialog suppressed on subsequent launches.
|
||||||
|
/// Caller persists this to UIPreferences alongside the existing
|
||||||
|
/// "shown welcome" flag.
|
||||||
|
/// </summary>
|
||||||
|
public bool SuppressFutureLaunches => DontShowAgain.IsChecked == true;
|
||||||
|
}
|
||||||
99
src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml
Normal file
99
src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<UserControl
|
||||||
|
x:Class="TeamsISO.App.WinUI.Views.SettingsDrawer"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Settings drawer — slides in from the right over the participants
|
||||||
|
table when the operator clicks the rail's settings icon. Esc dismiss.
|
||||||
|
Hosted inline (not as a separate Window) so the drawer feels like
|
||||||
|
part of the main surface rather than a satellite. Width fixed at
|
||||||
|
400px to give every setting a 320px input field after padding.
|
||||||
|
|
||||||
|
The five tabs mirror the WPF host's settings groups so the operator
|
||||||
|
finds the same toggles in the same places. The Appearance tab is
|
||||||
|
new — tri-state Theme picker (System / Dark / Light) plus a peek at
|
||||||
|
the accent palette so the operator can verify Wild Dragon brand is
|
||||||
|
respected on a light desk.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Grid Background="{ThemeResource BgSurface}"
|
||||||
|
BorderBrush="{ThemeResource BorderSubtle}"
|
||||||
|
BorderThickness="1,0,0,0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="56"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<Grid Grid.Row="0"
|
||||||
|
Padding="20,0,12,0"
|
||||||
|
BorderBrush="{ThemeResource BorderSubtle}"
|
||||||
|
BorderThickness="0,0,0,1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="Settings"
|
||||||
|
Style="{StaticResource TextTitle}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button x:Name="CloseButton"
|
||||||
|
Grid.Column="1"
|
||||||
|
Style="{StaticResource ButtonCaption}"
|
||||||
|
Click="OnCloseClick"
|
||||||
|
ToolTipService.ToolTip="Close (Esc)">
|
||||||
|
<FontIcon Glyph="" FontSize="12"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Tabs + body -->
|
||||||
|
<NavigationView Grid.Row="1"
|
||||||
|
x:Name="SettingsNav"
|
||||||
|
PaneDisplayMode="Top"
|
||||||
|
IsBackButtonVisible="Collapsed"
|
||||||
|
IsSettingsVisible="False"
|
||||||
|
OpenPaneLength="0"
|
||||||
|
SelectionChanged="OnTabSelectionChanged">
|
||||||
|
<NavigationView.MenuItems>
|
||||||
|
<NavigationViewItem Tag="Appearance" Content="Appearance" IsSelected="True"/>
|
||||||
|
<NavigationViewItem Tag="Routing" Content="Routing"/>
|
||||||
|
<NavigationViewItem Tag="Display" Content="Display"/>
|
||||||
|
<NavigationViewItem Tag="Control" Content="Control"/>
|
||||||
|
<NavigationViewItem Tag="Advanced" Content="Advanced"/>
|
||||||
|
</NavigationView.MenuItems>
|
||||||
|
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||||
|
Padding="20">
|
||||||
|
<StackPanel x:Name="TabContent" Spacing="16"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</NavigationView>
|
||||||
|
|
||||||
|
<!-- Footer: Apply / Reset -->
|
||||||
|
<Grid Grid.Row="2"
|
||||||
|
Padding="16,12"
|
||||||
|
BorderBrush="{ThemeResource BorderSubtle}"
|
||||||
|
BorderThickness="0,1,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
x:Name="DirtyHint"
|
||||||
|
Text="Changes apply on close."
|
||||||
|
Style="{StaticResource TextCaption}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Style="{StaticResource ButtonTertiary}"
|
||||||
|
Content="Reset to defaults"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
Click="OnResetClick"/>
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Style="{StaticResource ButtonPrimary}"
|
||||||
|
Content="Apply"
|
||||||
|
Click="OnApplyClick"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
229
src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml.cs
Normal file
229
src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml.cs
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue