feat(ui): rebuild MainWindow with Stone-theme design system

Adopts the design language from Dammyjay93/interface-design: warm Stone neutrals, accent orange (#EA580C), borders-only depth (no shadows), 8px spacing grid, all-caps section labels, mono typography for machine names and timecodes.

Themes/StoneTheme.xaml is the single source of truth for design tokens (color brushes, typography styles, spacing) plus restyled control templates (Button, TextBox, ComboBox, ComboBoxItem, CheckBox, DataGrid + DataGridColumnHeader / DataGridRow / DataGridCell, ScrollBar). MainWindow consumes the tokens and is laid out with a header (title + status pill), section-headed Settings sidebar (Output Format / NDI Network / Display), card-wrapped participant list, and a mono status footer.

Settings sidebar surfaces the new NDI group configuration (discovery + output) and a Hide-(Local) checkbox. The latter filters the user's own self-preview from the participants list at the MainViewModel layer (HideLocalSelf=true by default) so operators don't accidentally route their own preview as an ISO. Apply Changes round-trips both FrameProcessingSettings and NdiGroupSettings through the controller in one go.
This commit is contained in:
Zac Gaetano 2026-05-07 23:58:02 -04:00
parent 909237f454
commit b542d01835
5 changed files with 831 additions and 91 deletions

View file

@ -1,5 +1,11 @@
<Application x:Class="TeamsISO.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources/>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Themes/StoneTheme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View file

@ -3,60 +3,134 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:TeamsISO.App.ViewModels"
xmlns:conv="clr-namespace:TeamsISO.App.Converters"
Title="TeamsISO" Height="700" Width="1100"
Background="#202225" Foreground="#E8E8E8">
Title="TeamsISO"
Height="760" Width="1180"
MinHeight="600" MinWidth="960"
Background="{DynamicResource Stone.Canvas}"
UseLayoutRounding="True"
TextOptions.TextFormattingMode="Ideal"
TextOptions.TextRenderingMode="ClearType">
<Window.Resources>
<conv:BoolToVisibilityConverter x:Key="BoolToVis" />
<conv:EnumDescriptionConverter x:Key="EnumDesc" />
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#E8E8E8"/>
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="FontSize" Value="13"/>
</Style>
<Style TargetType="Button">
<Setter Property="Padding" Value="10,4"/>
<Setter Property="Margin" Value="0,4,0,0"/>
<Setter Property="Background" Value="#3F4147"/>
<Setter Property="Foreground" Value="#E8E8E8"/>
<Setter Property="BorderBrush" Value="#5A5C63"/>
</Style>
<Style TargetType="ComboBox">
<Setter Property="Margin" Value="0,2,0,8"/>
</Style>
<conv:BoolToVisibilityConverter x:Key="BoolToVis"/>
<conv:EnumDescriptionConverter x:Key="EnumDesc"/>
</Window.Resources>
<DockPanel>
<DockPanel LastChildFill="True">
<!-- Alert banner -->
<!-- ════════ Alert banner (top) ════════ -->
<Border DockPanel.Dock="Top"
Background="#7A2B2B"
Padding="12,8"
Background="{DynamicResource Status.Bad.Surface}"
BorderBrush="{DynamicResource Status.Bad}"
BorderThickness="0,0,0,1"
Padding="20,10"
Visibility="{Binding AlertBanner.IsVisible, Converter={StaticResource BoolToVis}}">
<DockPanel>
<Button DockPanel.Dock="Right"
Style="{StaticResource Button.Ghost}"
Content="Dismiss"
Command="{Binding AlertBanner.DismissCommand}"
Margin="12,0,0,0"/>
<TextBlock Text="{Binding AlertBanner.Message}"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse Width="8" Height="8"
Fill="{DynamicResource Status.Bad}"
VerticalAlignment="Center"
Margin="0,0,10,0"/>
<TextBlock Text="{Binding AlertBanner.Message}"
Style="{StaticResource Text.Body}"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
</StackPanel>
</DockPanel>
</Border>
<!-- Status footer -->
<Border DockPanel.Dock="Bottom" Background="#191B1F" Padding="10,6">
<TextBlock Text="{Binding StatusText}" FontStyle="Italic" Foreground="#A0A0A0"/>
<!-- ════════ Header ════════ -->
<Border DockPanel.Dock="Top"
Background="{DynamicResource Stone.Canvas}"
BorderBrush="{DynamicResource Stone.Border}"
BorderThickness="0,0,0,1"
Padding="24,18">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="TeamsISO"
Style="{StaticResource Text.Display}"
FontWeight="Bold"
VerticalAlignment="Center"/>
<TextBlock Text="Per-participant NDI ISO controller"
Style="{StaticResource Text.Body}"
Foreground="{DynamicResource Text.Tertiary}"
Margin="14,0,0,0"
VerticalAlignment="Center"/>
</StackPanel>
<!-- Engine status pill, right-aligned -->
<Border Grid.Column="1"
Style="{StaticResource Pill.Online}"
VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<Ellipse Width="7" Height="7"
Fill="{DynamicResource Status.Good}"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding StatusText}"
Style="{StaticResource Text.Body}"
FontSize="12"
Foreground="{DynamicResource Text.Primary}"
Margin="8,0,0,0"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</Border>
<!-- Settings sidebar -->
<Border DockPanel.Dock="Right" Background="#2A2C32" Width="320" Padding="14">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock Text="Global Settings" FontSize="16" FontWeight="Bold" Margin="0,0,0,12"/>
<!-- ════════ Footer / status bar ════════ -->
<Border DockPanel.Dock="Bottom"
Background="{DynamicResource Stone.Canvas}"
BorderBrush="{DynamicResource Stone.Border}"
BorderThickness="0,1,0,0"
Padding="24,10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding StatusText}"
Style="{StaticResource Text.Mono}"
Foreground="{DynamicResource Text.Tertiary}"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="1"
Text="Wild Dragon LLC · 1.0.0-alpha"
Style="{StaticResource Text.Mono}"
Foreground="{DynamicResource Text.Tertiary}"
VerticalAlignment="Center"/>
</Grid>
</Border>
<TextBlock Text="Target Framerate"/>
<!-- ════════ Settings panel (right) ════════ -->
<Border DockPanel.Dock="Right"
Width="360"
Background="{DynamicResource Stone.Surface}"
BorderBrush="{DynamicResource Stone.Border}"
BorderThickness="1,0,0,0">
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="0">
<StackPanel Margin="24,24,24,32">
<TextBlock Text="Settings"
Style="{StaticResource Text.Heading}"
FontSize="16"
Margin="0,0,0,20"/>
<!-- · · Output Format · · -->
<TextBlock Text="OUTPUT FORMAT" Style="{StaticResource Text.Caption}"/>
<TextBlock Text="Target Framerate"
Style="{StaticResource Text.Body}"
Foreground="{DynamicResource Text.Secondary}"
Margin="0,8,0,4"/>
<ComboBox ItemsSource="{Binding Settings.AvailableFramerates}"
SelectedItem="{Binding Settings.Framerate}">
<ComboBox.ItemTemplate>
@ -66,7 +140,10 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="Target Resolution"/>
<TextBlock Text="Target Resolution"
Style="{StaticResource Text.Body}"
Foreground="{DynamicResource Text.Secondary}"
Margin="0,12,0,4"/>
<ComboBox ItemsSource="{Binding Settings.AvailableResolutions}"
SelectedItem="{Binding Settings.Resolution}">
<ComboBox.ItemTemplate>
@ -76,7 +153,10 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="Aspect Mode"/>
<TextBlock Text="Aspect Mode"
Style="{StaticResource Text.Body}"
Foreground="{DynamicResource Text.Secondary}"
Margin="0,12,0,4"/>
<ComboBox ItemsSource="{Binding Settings.AvailableAspectModes}"
SelectedItem="{Binding Settings.Aspect}">
<ComboBox.ItemTemplate>
@ -86,7 +166,10 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="Audio Mode"/>
<TextBlock Text="Audio Mode"
Style="{StaticResource Text.Body}"
Foreground="{DynamicResource Text.Secondary}"
Margin="0,12,0,4"/>
<ComboBox ItemsSource="{Binding Settings.AvailableAudioModes}"
SelectedItem="{Binding Settings.Audio}">
<ComboBox.ItemTemplate>
@ -96,63 +179,187 @@
</ComboBox.ItemTemplate>
</ComboBox>
<Button Content="Apply" Command="{Binding Settings.ApplyCommand}" Margin="0,16,0,0"/>
<!-- · · NDI Network · · -->
<TextBlock Text="NDI NETWORK"
Style="{StaticResource Text.Caption}"
Margin="0,28,0,0"/>
<TextBlock Text="Discovery group"
Style="{StaticResource Text.Body}"
Foreground="{DynamicResource Text.Secondary}"
Margin="0,8,0,4"/>
<TextBox Text="{Binding Settings.DiscoveryGroups, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Receive sources from this group. Empty = Public."
Style="{StaticResource Text.Body}"
FontSize="11"
Foreground="{DynamicResource Text.Tertiary}"
Margin="0,4,0,0"/>
<TextBlock Text="Output group"
Style="{StaticResource Text.Body}"
Foreground="{DynamicResource Text.Secondary}"
Margin="0,12,0,4"/>
<TextBox Text="{Binding Settings.OutputGroups, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Broadcast TeamsISO outputs on this group. Empty = Public."
Style="{StaticResource Text.Body}"
FontSize="11"
Foreground="{DynamicResource Text.Tertiary}"
Margin="0,4,0,0"/>
<Border Background="{DynamicResource Stone.SurfaceElevated}"
BorderBrush="{DynamicResource Stone.Border}"
BorderThickness="1"
CornerRadius="{StaticResource Radius.M}"
Padding="12,10"
Margin="0,12,0,0">
<TextBlock Style="{StaticResource Text.Body}"
FontSize="11"
Foreground="{DynamicResource Text.Secondary}"
TextWrapping="Wrap"
Text="Group changes apply on next launch — running pipelines aren't restarted to avoid orphaning your live ISOs."/>
</Border>
<!-- · · Display · · -->
<TextBlock Text="DISPLAY"
Style="{StaticResource Text.Caption}"
Margin="0,28,0,0"/>
<CheckBox Content="Hide my self-preview from participants"
IsChecked="{Binding Settings.HideLocalSelf}"
Margin="0,8,0,0"/>
<Button Style="{StaticResource Button.Primary}"
Content="Apply Changes"
Command="{Binding Settings.ApplyCommand}"
HorizontalAlignment="Stretch"
Margin="0,28,0,0"
Padding="0,10"/>
</StackPanel>
</ScrollViewer>
</Border>
<!-- Main content: participants list -->
<Grid Margin="14">
<!-- ════════ Main content: participants ════════ -->
<Grid Margin="32,28,32,28">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="Participants"
FontSize="16" FontWeight="Bold"
Margin="0,0,0,8"/>
<!-- Section header with count -->
<StackPanel Grid.Row="0" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Participants"
Style="{StaticResource Text.Heading}"
FontSize="18"/>
<Border Background="{DynamicResource Stone.Surface}"
BorderBrush="{DynamicResource Stone.Border}"
BorderThickness="1"
CornerRadius="999"
Padding="9,2"
Margin="12,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding Participants.Count}"
Style="{StaticResource Text.Mono}"
Foreground="{DynamicResource Text.Secondary}"
FontSize="11"/>
</Border>
</StackPanel>
<DataGrid Grid.Row="1"
ItemsSource="{Binding Participants}"
AutoGenerateColumns="False"
Background="#2A2C32"
Foreground="#E8E8E8"
RowBackground="#2A2C32"
AlternatingRowBackground="#2E3037"
BorderBrush="#5A5C63"
GridLinesVisibility="Horizontal"
HeadersVisibility="Column"
CanUserAddRows="False" CanUserDeleteRows="False"
RowHeight="34">
<DataGrid.Columns>
<DataGridTextColumn Header="Display Name" Binding="{Binding DisplayName}" Width="2*" IsReadOnly="True"/>
<DataGridTextColumn Header="Source" Binding="{Binding SourceMachine}" Width="*" IsReadOnly="True"/>
<DataGridTextColumn Header="ISO Output Name" Binding="{Binding CustomName, UpdateSourceTrigger=PropertyChanged}" Width="2*"/>
<DataGridTemplateColumn Header="ISO" Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="{Binding IsEnabled, StringFormat={}{0}}" Command="{Binding ToggleIsoCommand}">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Content" Value="Enable"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled}" Value="True">
<Setter Property="Content" Value="Disable"/>
<Setter Property="Background" Value="#3D6F45"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsProcessing}" Value="True">
<Setter Property="Content" Value="…"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<TextBlock Grid.Row="1"
Text="Toggle the ISO column to spin up an isolated, normalized NDI output for any participant."
Style="{StaticResource Text.Body}"
Foreground="{DynamicResource Text.Tertiary}"
Margin="0,6,0,18"/>
<!-- Participants table inside a card -->
<Border Grid.Row="2"
Style="{StaticResource Card}"
Padding="0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DataGrid Grid.Row="1"
ItemsSource="{Binding Participants}"
Margin="4,4,4,4">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Display Name" Width="2*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Border Width="28" Height="28"
CornerRadius="14"
Background="{DynamicResource Accent.OrangeMuted}">
<TextBlock Text="●"
FontSize="9"
Foreground="{DynamicResource Accent.Orange}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<TextBlock Text="{Binding DisplayName}"
Style="{StaticResource Text.Body}"
FontWeight="Medium"
Margin="12,0,0,0"
VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Source" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding SourceMachine}"
Style="{StaticResource Text.Mono}"
VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Output Name" Width="2*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding CustomName, UpdateSourceTrigger=PropertyChanged}"
Padding="8,6"
Margin="0,0,12,0"
VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="ISO" Width="120">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Command="{Binding ToggleIsoCommand}"
Margin="0,0,12,0">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource Button.IsoToggle}">
<Setter Property="Content" Value="Enable"/>
<Setter Property="Foreground" Value="{DynamicResource Text.Secondary}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled}" Value="True">
<Setter Property="Content" Value="● Live"/>
<Setter Property="Background" Value="#1F3A1F"/>
<Setter Property="BorderBrush" Value="{DynamicResource Status.Good}"/>
<Setter Property="Foreground" Value="{DynamicResource Status.Good}"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsProcessing}" Value="True">
<Setter Property="Content" Value="…"/>
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
</Grid>
</DockPanel>

View file

@ -0,0 +1,491 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<!--
TeamsISO design tokens — adapted from the "interface-design" Stone aesthetic:
warm neutrals, borders-only depth (no shadows), accent orange, 8px spacing grid,
tight typography. Manrope is the reference font; we fall back to Segoe UI
Variable Display which ships with Windows 11 and reads similarly.
-->
<!-- ═════ Spacing (8px grid) ═════ -->
<sys:Double x:Key="Spacing.XS">4</sys:Double>
<sys:Double x:Key="Spacing.S">8</sys:Double>
<sys:Double x:Key="Spacing.M">12</sys:Double>
<sys:Double x:Key="Spacing.L">16</sys:Double>
<sys:Double x:Key="Spacing.XL">24</sys:Double>
<sys:Double x:Key="Spacing.XXL">32</sys:Double>
<!-- ═════ Radii ═════ -->
<CornerRadius x:Key="Radius.S">4</CornerRadius>
<CornerRadius x:Key="Radius.M">6</CornerRadius>
<CornerRadius x:Key="Radius.L">8</CornerRadius>
<!-- ═════ Stone palette (dark theme — primary look) ═════ -->
<SolidColorBrush x:Key="Stone.Canvas" Color="#1B1916"/>
<SolidColorBrush x:Key="Stone.Surface" Color="#26221F"/>
<SolidColorBrush x:Key="Stone.SurfaceElevated" Color="#2D2926"/>
<SolidColorBrush x:Key="Stone.SurfaceHover" Color="#34302D"/>
<SolidColorBrush x:Key="Stone.Border" Color="#3A3633"/>
<SolidColorBrush x:Key="Stone.BorderStrong" Color="#4D4742"/>
<SolidColorBrush x:Key="Text.Primary" Color="#F5F3F0"/>
<SolidColorBrush x:Key="Text.Secondary" Color="#B5AFA8"/>
<SolidColorBrush x:Key="Text.Tertiary" Color="#857F77"/>
<SolidColorBrush x:Key="Text.Disabled" Color="#5C5751"/>
<SolidColorBrush x:Key="Accent.Orange" Color="#EA580C"/>
<SolidColorBrush x:Key="Accent.OrangeHover" Color="#F97316"/>
<SolidColorBrush x:Key="Accent.OrangeMuted" Color="#7A2F0E"/>
<SolidColorBrush x:Key="Status.Good" Color="#84CC16"/>
<SolidColorBrush x:Key="Status.Warn" Color="#F59E0B"/>
<SolidColorBrush x:Key="Status.Bad" Color="#DC2626"/>
<SolidColorBrush x:Key="Status.Bad.Surface" Color="#3F1414"/>
<!-- ═════ Typography ═════ -->
<FontFamily x:Key="Font.Sans">Segoe UI Variable Display, Segoe UI, Manrope, sans-serif</FontFamily>
<FontFamily x:Key="Font.Mono">Cascadia Mono, Consolas, monospace</FontFamily>
<!-- Display: section headers, hero stats. -->
<Style x:Key="Text.Display" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource Font.Sans}"/>
<Setter Property="FontSize" Value="22"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource Text.Primary}"/>
<Setter Property="TextOptions.TextFormattingMode" Value="Ideal"/>
<Setter Property="TextOptions.TextRenderingMode" Value="ClearType"/>
</Style>
<Style x:Key="Text.Heading" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource Font.Sans}"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource Text.Primary}"/>
<Setter Property="TextOptions.TextFormattingMode" Value="Ideal"/>
</Style>
<Style x:Key="Text.Body" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource Font.Sans}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="Normal"/>
<Setter Property="Foreground" Value="{StaticResource Text.Primary}"/>
<Setter Property="TextOptions.TextFormattingMode" Value="Ideal"/>
</Style>
<!-- Caption: labels above inputs, secondary metadata. -->
<Style x:Key="Text.Caption" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource Font.Sans}"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="Foreground" Value="{StaticResource Text.Secondary}"/>
<Setter Property="TextOptions.TextFormattingMode" Value="Ideal"/>
<Setter Property="Typography.Capitals" Value="AllSmallCaps"/>
<Setter Property="Margin" Value="0,0,0,4"/>
</Style>
<!-- Mono: machine names, hex IDs, timecodes. -->
<Style x:Key="Text.Mono" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource Font.Mono}"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="{StaticResource Text.Secondary}"/>
</Style>
<!-- ═════ TextBlock default ═════ -->
<Style TargetType="TextBlock" BasedOn="{StaticResource Text.Body}"/>
<!-- ═════ Button: ghost/secondary (default) ═════ -->
<Style x:Key="Button.Ghost" TargetType="Button">
<Setter Property="FontFamily" Value="{StaticResource Font.Sans}"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="Foreground" Value="{StaticResource Text.Primary}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource Stone.Border}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="12,7"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{StaticResource Radius.M}"
SnapsToDevicePixels="True">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource Stone.SurfaceHover}"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource Stone.BorderStrong}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource Stone.SurfaceElevated}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{StaticResource Text.Disabled}"/>
<Setter TargetName="Bd" Property="Opacity" Value="0.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ═════ Button: primary (orange accent) ═════ -->
<Style x:Key="Button.Primary" TargetType="Button" BasedOn="{StaticResource Button.Ghost}">
<Setter Property="Background" Value="{StaticResource Accent.Orange}"/>
<Setter Property="BorderBrush" Value="{StaticResource Accent.Orange}"/>
<Setter Property="Foreground" Value="#FFFFFF"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource Accent.OrangeHover}"/>
<Setter Property="BorderBrush" Value="{StaticResource Accent.OrangeHover}"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- ═════ Button: ISO toggle (pill-shaped, status-coded) ═════ -->
<Style x:Key="Button.IsoToggle" TargetType="Button">
<Setter Property="FontFamily" Value="{StaticResource Font.Sans}"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource Text.Primary}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource Stone.BorderStrong}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="10,5"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="MinWidth" Value="76"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="999"
SnapsToDevicePixels="True">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="{TemplateBinding Padding}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource Stone.SurfaceHover}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ═════ TextBox ═════ -->
<Style x:Key="TextBox.Default" TargetType="TextBox">
<Setter Property="FontFamily" Value="{StaticResource Font.Sans}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="{StaticResource Text.Primary}"/>
<Setter Property="Background" Value="{StaticResource Stone.Surface}"/>
<Setter Property="CaretBrush" Value="{StaticResource Accent.Orange}"/>
<Setter Property="SelectionBrush" Value="{StaticResource Accent.OrangeMuted}"/>
<Setter Property="BorderBrush" Value="{StaticResource Stone.Border}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="10,7"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{StaticResource Radius.M}"
SnapsToDevicePixels="True">
<ScrollViewer x:Name="PART_ContentHost"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
Focusable="False"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource Accent.Orange}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource Stone.BorderStrong}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="TextBox" BasedOn="{StaticResource TextBox.Default}"/>
<!-- ═════ ComboBox ═════ -->
<Style x:Key="ComboBoxToggleStripped" TargetType="ToggleButton">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="ClickMode" Value="Press"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid>
<Path x:Name="Arrow"
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0,0,12,0"
Data="M 0,0 L 4,4 L 8,0 Z"
Fill="{StaticResource Text.Secondary}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ComboBox.Default" TargetType="ComboBox">
<Setter Property="FontFamily" Value="{StaticResource Font.Sans}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="{StaticResource Text.Primary}"/>
<Setter Property="Background" Value="{StaticResource Stone.Surface}"/>
<Setter Property="BorderBrush" Value="{StaticResource Stone.Border}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="10,7"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{StaticResource Radius.M}"
SnapsToDevicePixels="True"/>
<ToggleButton Style="{StaticResource ComboBoxToggleStripped}"
IsChecked="{Binding IsDropDownOpen,
RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/>
<ContentPresenter Margin="{TemplateBinding Padding}"
IsHitTestVisible="False"
VerticalAlignment="Center"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"/>
<Popup x:Name="PART_Popup"
IsOpen="{TemplateBinding IsDropDownOpen}"
Placement="Bottom"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="None">
<Border Background="{StaticResource Stone.SurfaceElevated}"
BorderBrush="{StaticResource Stone.BorderStrong}"
BorderThickness="1"
CornerRadius="{StaticResource Radius.M}"
MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="280"
Margin="0,4,0,0">
<ScrollViewer>
<ItemsPresenter/>
</ScrollViewer>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource Stone.BorderStrong}"/>
</Trigger>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource Accent.Orange}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ComboBox" BasedOn="{StaticResource ComboBox.Default}"/>
<!-- ComboBoxItem -->
<Style TargetType="ComboBoxItem">
<Setter Property="Foreground" Value="{StaticResource Text.Primary}"/>
<Setter Property="Padding" Value="10,7"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Border x:Name="Bd"
Background="Transparent"
CornerRadius="{StaticResource Radius.S}"
Margin="4,2"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource Stone.SurfaceHover}"/>
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource Stone.SurfaceHover}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ═════ CheckBox (custom, square+orange-tick when checked) ═════ -->
<Style TargetType="CheckBox">
<Setter Property="FontFamily" Value="{StaticResource Font.Sans}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="{StaticResource Text.Primary}"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Padding" Value="8,0,0,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<StackPanel Orientation="Horizontal">
<Border x:Name="Box"
Width="16" Height="16"
BorderBrush="{StaticResource Stone.BorderStrong}"
BorderThickness="1"
Background="{StaticResource Stone.Surface}"
CornerRadius="3"
VerticalAlignment="Center">
<Path x:Name="Tick"
Data="M 3,8 L 6.5,11.5 L 13,4"
Stroke="#FFFFFF" StrokeThickness="2"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Visibility="Collapsed"/>
</Border>
<ContentPresenter Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Box" Property="Background" Value="{StaticResource Accent.Orange}"/>
<Setter TargetName="Box" Property="BorderBrush" Value="{StaticResource Accent.Orange}"/>
<Setter TargetName="Tick" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Box" Property="BorderBrush" Value="{StaticResource Accent.OrangeHover}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ═════ Card ═════ -->
<Style x:Key="Card" TargetType="Border">
<Setter Property="Background" Value="{StaticResource Stone.Surface}"/>
<Setter Property="BorderBrush" Value="{StaticResource Stone.Border}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{StaticResource Radius.L}"/>
<Setter Property="Padding" Value="{StaticResource Spacing.L}"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
</Style>
<!-- ═════ DataGrid (re-skinned) ═════ -->
<Style TargetType="DataGrid">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource Text.Primary}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="GridLinesVisibility" Value="None"/>
<Setter Property="HeadersVisibility" Value="Column"/>
<Setter Property="RowHeight" Value="48"/>
<Setter Property="RowBackground" Value="Transparent"/>
<Setter Property="AlternatingRowBackground" Value="Transparent"/>
<Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="CanUserAddRows" Value="False"/>
<Setter Property="CanUserDeleteRows" Value="False"/>
<Setter Property="CanUserResizeRows" Value="False"/>
<Setter Property="SelectionMode" Value="Single"/>
<Setter Property="SelectionUnit" Value="FullRow"/>
<Setter Property="AutoGenerateColumns" Value="False"/>
</Style>
<Style TargetType="DataGridColumnHeader">
<Setter Property="FontFamily" Value="{StaticResource Font.Sans}"/>
<Setter Property="FontSize" Value="10"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="Foreground" Value="{StaticResource Text.Tertiary}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource Stone.Border}"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="Padding" Value="12,10"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="Typography.Capitals" Value="AllSmallCaps"/>
</Style>
<Style TargetType="DataGridRow">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource Stone.Border}"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource Stone.SurfaceHover}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{StaticResource Stone.SurfaceElevated}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="DataGridCell">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="12,0"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="{StaticResource Text.Primary}"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="DataGridCell">
<Border Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<ContentPresenter VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ScrollBar (slim) -->
<Style TargetType="ScrollBar">
<Setter Property="Width" Value="8"/>
<Setter Property="MinWidth" Value="8"/>
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers>
<Trigger Property="Orientation" Value="Horizontal">
<Setter Property="Height" Value="8"/>
<Setter Property="MinHeight" Value="8"/>
<Setter Property="Width" Value="Auto"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- ═════ Status pill ═════ -->
<Style x:Key="Pill.Online" TargetType="Border">
<Setter Property="Background" Value="#1F3A1F"/>
<Setter Property="CornerRadius" Value="999"/>
<Setter Property="Padding" Value="10,3"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
<Style x:Key="Pill.Offline" TargetType="Border">
<Setter Property="Background" Value="{StaticResource Status.Bad.Surface}"/>
<Setter Property="CornerRadius" Value="999"/>
<Setter Property="Padding" Value="10,3"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
</ResourceDictionary>

View file

@ -4,7 +4,8 @@ using TeamsISO.Engine.Domain;
namespace TeamsISO.App.ViewModels;
/// <summary>
/// Bindings for the global settings panel: framerate, resolution, aspect, audio.
/// Bindings for the global settings panel: framerate, resolution, aspect, audio,
/// NDI groups (discovery + output), and the participant-list hide-Local toggle.
/// </summary>
public sealed class GlobalSettingsViewModel : ObservableObject
{
@ -13,6 +14,9 @@ public sealed class GlobalSettingsViewModel : ObservableObject
private TargetResolution _resolution;
private AspectMode _aspect;
private AudioMode _audio;
private string _discoveryGroups;
private string _outputGroups;
private bool _hideLocalSelf = true;
public GlobalSettingsViewModel(IIsoController controller)
{
@ -22,6 +26,11 @@ public sealed class GlobalSettingsViewModel : ObservableObject
_resolution = current.Resolution;
_aspect = current.Aspect;
_audio = current.Audio;
var groups = controller.GroupSettings;
_discoveryGroups = groups.DiscoveryGroups ?? string.Empty;
_outputGroups = groups.OutputGroups ?? string.Empty;
ApplyCommand = new AsyncRelayCommand(ApplyAsync);
}
@ -35,11 +44,29 @@ public sealed class GlobalSettingsViewModel : ObservableObject
public AspectMode Aspect { get => _aspect; set => SetField(ref _aspect, value); }
public AudioMode Audio { get => _audio; set => SetField(ref _audio, value); }
/// <summary>NDI discovery group(s) — comma-separated. Empty = default (Public).</summary>
public string DiscoveryGroups { get => _discoveryGroups; set => SetField(ref _discoveryGroups, value); }
/// <summary>NDI output group(s) — comma-separated. Empty = default (Public).</summary>
public string OutputGroups { get => _outputGroups; set => SetField(ref _outputGroups, value); }
/// <summary>
/// Hide the user's own self-preview ("(Local)") from the participants list.
/// On by default — operators rarely want to ISO-route their own preview.
/// Read by <see cref="MainViewModel"/> when filtering the list it presents.
/// </summary>
public bool HideLocalSelf { get => _hideLocalSelf; set => SetField(ref _hideLocalSelf, value); }
public AsyncRelayCommand ApplyCommand { get; }
private Task ApplyAsync()
private async Task ApplyAsync()
{
var settings = new FrameProcessingSettings(_framerate, _resolution, _aspect, _audio);
return _controller.SetGlobalSettingsAsync(settings, CancellationToken.None);
await _controller.SetGlobalSettingsAsync(settings, CancellationToken.None);
var groups = new NdiGroupSettings(
DiscoveryGroups: string.IsNullOrWhiteSpace(_discoveryGroups) ? null : _discoveryGroups.Trim(),
OutputGroups: string.IsNullOrWhiteSpace(_outputGroups) ? null : _outputGroups.Trim());
await _controller.SetGroupSettingsAsync(groups, CancellationToken.None);
}
}

View file

@ -61,8 +61,14 @@ public sealed class MainViewModel : ObservableObject, IDisposable
private void OnParticipantsChanged(IReadOnlyList<Participant> incoming)
{
var seenIds = new HashSet<Guid>();
var hideLocal = Settings.HideLocalSelf;
foreach (var p in incoming)
{
// The new Teams client emits a "(Local)" pseudo-participant for the user's
// own preview — operators rarely want it as a routable ISO. Suppress when
// HideLocalSelf is on (default).
if (hideLocal && IsLocalSelf(p)) continue;
seenIds.Add(p.Id);
if (_byId.TryGetValue(p.Id, out var vm))
{
@ -76,7 +82,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
}
}
// Remove participants no longer present
// Remove participants no longer present (or now hidden by the filter).
for (var i = Participants.Count - 1; i >= 0; i--)
{
var vm = Participants[i];
@ -88,6 +94,9 @@ public sealed class MainViewModel : ObservableObject, IDisposable
}
}
private static bool IsLocalSelf(Participant p) =>
string.Equals(p.DisplayName, "(Local)", StringComparison.Ordinal);
public void Dispose()
{
_participantsSub.Dispose();