teamsiso/src/TeamsISO.App/MainWindow.xaml

790 lines
46 KiB
Text
Raw Normal View History

<Window x:Class="TeamsISO.App.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:TeamsISO.App.ViewModels"
xmlns:conv="clr-namespace:TeamsISO.App.Converters"
mc:Ignorable="d"
Title="TeamsISO"
Width="1280" Height="780"
MinWidth="980" MinHeight="640"
WindowStartupLocation="CenterScreen"
Background="{DynamicResource Wd.Canvas}"
Foreground="{DynamicResource Wd.Text.Primary}"
UseLayoutRounding="True"
SnapsToDevicePixels="True"
TextOptions.TextRenderingMode="ClearType"
TextOptions.TextFormattingMode="Display"
d:DataContext="{d:DesignInstance Type=vm:MainViewModel}">
<!--
v2 SHELL — "Studio Terminal"
(Approved 2026-05-13. Shape brief at docs/shapes/2026-05-13-teamsiso-v2-studio-terminal.md)
Default Windows title bar (no chromeless WindowChrome). The 32px header
below it carries the brand mark, wordmark, and three icon buttons:
⌘K (command palette), theme toggle, settings drawer. Below that, a
single transport strip carries the operator's at-a-glance status.
The participants area is the canvas — no rail, no permanent side
panel, no footer. The meeting bar at the bottom renders ONLY when
Teams is in a call.
-->
<Window.Resources>
<conv:BoolToVisibilityConverter x:Key="BoolToVis"
TrueValue="Visible"
FalseValue="Collapsed"/>
<conv:BoolToVisibilityConverter x:Key="BoolToVisInverse"
TrueValue="Collapsed"
FalseValue="Visible"/>
<conv:EnumDescriptionConverter x:Key="EnumDesc"/>
</Window.Resources>
<Window.InputBindings>
<KeyBinding Key="F1" Command="{Binding ShowHelpCommand}"/>
<KeyBinding Key="S" Modifiers="Ctrl+Shift" Command="{Binding StopAllIsosCommand}"/>
<KeyBinding Key="R" Modifiers="Ctrl" Command="{Binding RefreshDiscoveryCommand}"/>
<KeyBinding Key="T" Modifiers="Ctrl" Command="{Binding ToggleThemeCommand}"/>
<!-- Ctrl+K — command palette (next session). For now opens the same
settings drawer surface; replaced with the palette window once
it lands. -->
<KeyBinding Key="K" Modifiers="Ctrl" Command="{Binding ShowHelpCommand}"/>
<KeyBinding Key="NumPad1" Command="{Binding ToggleByIndexCommand}" CommandParameter="1"/>
<KeyBinding Key="NumPad2" Command="{Binding ToggleByIndexCommand}" CommandParameter="2"/>
<KeyBinding Key="NumPad3" Command="{Binding ToggleByIndexCommand}" CommandParameter="3"/>
<KeyBinding Key="NumPad4" Command="{Binding ToggleByIndexCommand}" CommandParameter="4"/>
<KeyBinding Key="NumPad5" Command="{Binding ToggleByIndexCommand}" CommandParameter="5"/>
<KeyBinding Key="NumPad6" Command="{Binding ToggleByIndexCommand}" CommandParameter="6"/>
<KeyBinding Key="NumPad7" Command="{Binding ToggleByIndexCommand}" CommandParameter="7"/>
<KeyBinding Key="NumPad8" Command="{Binding ToggleByIndexCommand}" CommandParameter="8"/>
<KeyBinding Key="NumPad9" Command="{Binding ToggleByIndexCommand}" CommandParameter="9"/>
<KeyBinding Key="D1" Command="{Binding ToggleByIndexCommand}" CommandParameter="1"/>
<KeyBinding Key="D2" Command="{Binding ToggleByIndexCommand}" CommandParameter="2"/>
<KeyBinding Key="D3" Command="{Binding ToggleByIndexCommand}" CommandParameter="3"/>
<KeyBinding Key="D4" Command="{Binding ToggleByIndexCommand}" CommandParameter="4"/>
<KeyBinding Key="D5" Command="{Binding ToggleByIndexCommand}" CommandParameter="5"/>
<KeyBinding Key="D6" Command="{Binding ToggleByIndexCommand}" CommandParameter="6"/>
<KeyBinding Key="D7" Command="{Binding ToggleByIndexCommand}" CommandParameter="7"/>
<KeyBinding Key="D8" Command="{Binding ToggleByIndexCommand}" CommandParameter="8"/>
<KeyBinding Key="D9" Command="{Binding ToggleByIndexCommand}" CommandParameter="9"/>
</Window.InputBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="40"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- ═════════════════════ HEADER (32px) ═════════════════════ -->
<Border Grid.Row="0"
Background="{DynamicResource Wd.Canvas}"
BorderBrush="{DynamicResource Wd.Border}"
BorderThickness="0,0,0,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Brand: Wild Dragon mark + wordmark. Click on the mark opens About.
Mark is small (20px) and sits as a quality cue — not as
marketing chrome. -->
<StackPanel Grid.Column="0"
Orientation="Horizontal"
VerticalAlignment="Center"
Margin="12,0,0,0">
<Button Click="OnAboutClick"
Padding="2"
Background="Transparent"
BorderThickness="0"
Cursor="Hand"
ToolTip="About TeamsISO">
feat: app icon, FPS, drops counter, --version, About dialog, Stop Teams toggle Six related polish items, all building on tonight's groundwork. 1. App icon: teamsiso.ico generated from dragon-mark.png at 7 sizes (16-256), wired as ApplicationIcon in the WPF csproj, MainWindow.Icon, AboutWindow.Icon, and ARPPRODUCTICON in the WiX MSI. Taskbar / window / Add-Remove-Programs all show the dragon mark now. 2. Running incoming FPS: ring buffer of last 30 frame timestamps in IsoPipeline; ComputeFps() returns moving-average rate. Surfaced on IsoHealthStats.IncomingFps and shown in the Source column of the participants DataGrid as 'WxH · 59.94 fps'. Resets cleanly on every supervisor restart. 3. Drops counter: FrameProcessor.Stats already aggregated FramesDropped (closest-frame strategy when the receiver outpaces the processor) and FramesDuplicated; just plumbed _liveProcessor through IsoPipeline so GetStats() can read them. Exposed in the Live column under the in/out counters as a coral-tinted 'drop N'. 4. Console --version flag: prints engine version (with embedded git SHA), .NET version, OS, NDI runtime banner, expected prefix, exit-code legend, plus a wilddragon.net link. Useful for support tickets. 5. About dialog: chromeless modal with the dragon mark + version / .NET / OS / NDI runtime fields and a link to wilddragon.net. Triggered by clicking the rail logo. 6. Teams launcher Stop toggle: TeamsLauncher gains IsRunning() and StopAll(). The rail's Teams button now toggles — if Teams is up, ask to close all Teams windows via WM_CLOSE; otherwise launch as before. Confirms before stopping so we don't kill the user's call mid-transition. Tests: 74/74 unit + 9/9 NDI integration green throughout. MSI builds clean and now embeds the dragon icon for ARP.
2026-05-08 13:50:19 -04:00
<Image Source="/Assets/dragon-mark.png"
Width="20" Height="20"
feat: app icon, FPS, drops counter, --version, About dialog, Stop Teams toggle Six related polish items, all building on tonight's groundwork. 1. App icon: teamsiso.ico generated from dragon-mark.png at 7 sizes (16-256), wired as ApplicationIcon in the WPF csproj, MainWindow.Icon, AboutWindow.Icon, and ARPPRODUCTICON in the WiX MSI. Taskbar / window / Add-Remove-Programs all show the dragon mark now. 2. Running incoming FPS: ring buffer of last 30 frame timestamps in IsoPipeline; ComputeFps() returns moving-average rate. Surfaced on IsoHealthStats.IncomingFps and shown in the Source column of the participants DataGrid as 'WxH · 59.94 fps'. Resets cleanly on every supervisor restart. 3. Drops counter: FrameProcessor.Stats already aggregated FramesDropped (closest-frame strategy when the receiver outpaces the processor) and FramesDuplicated; just plumbed _liveProcessor through IsoPipeline so GetStats() can read them. Exposed in the Live column under the in/out counters as a coral-tinted 'drop N'. 4. Console --version flag: prints engine version (with embedded git SHA), .NET version, OS, NDI runtime banner, expected prefix, exit-code legend, plus a wilddragon.net link. Useful for support tickets. 5. About dialog: chromeless modal with the dragon mark + version / .NET / OS / NDI runtime fields and a link to wilddragon.net. Triggered by clicking the rail logo. 6. Teams launcher Stop toggle: TeamsLauncher gains IsRunning() and StopAll(). The rail's Teams button now toggles — if Teams is up, ask to close all Teams windows via WM_CLOSE; otherwise launch as before. Confirms before stopping so we don't kill the user's call mid-transition. Tests: 74/74 unit + 9/9 NDI integration green throughout. MSI builds clean and now embeds the dragon icon for ARP.
2026-05-08 13:50:19 -04:00
RenderOptions.BitmapScalingMode="HighQuality"/>
</Button>
<TextBlock Text="TeamsISO"
FontFamily="{StaticResource Wd.Font.Sans}"
FontSize="13"
FontWeight="Medium"
Foreground="{DynamicResource Wd.Text.Primary}"
VerticalAlignment="Center"
Margin="8,0,0,0"/>
</StackPanel>
<!-- Right cluster: three icon buttons. ⌘K opens the command
palette (Ctrl+K shortcut). The theme button cycles
dark ↔ light (Ctrl+T). The gear opens the settings
drawer. That's the entire chrome. -->
<StackPanel Grid.Column="2"
Orientation="Horizontal"
VerticalAlignment="Center"
Margin="0,0,10,0">
<Button Style="{StaticResource Wd.Button.Ghost}"
Click="OnCommandPaletteClick"
Padding="8,4"
Margin="0,0,2,0"
ToolTip="Command palette (Ctrl+K)"
FontFamily="{StaticResource Wd.Font.Mono}"
FontSize="11"
Foreground="{DynamicResource Wd.Text.Secondary}"
Content="⌘K"/>
<Button Style="{StaticResource Wd.Button.Ghost}"
Command="{Binding ToggleThemeCommand}"
Padding="6,4"
Margin="0,0,2,0"
ToolTip="Toggle theme (Ctrl+T)">
<Path Data="M 8,2 C 8,2 4,4 4,8 C 4,12 8,14 8,14 C 5,14 2,11 2,8 C 2,5 5,2 8,2 Z"
Stroke="{DynamicResource Wd.Text.Secondary}"
StrokeThickness="1.4"
Fill="{DynamicResource Wd.Text.Secondary}"
Width="14" Height="14"
Stretch="None"/>
</Button>
<Button Style="{StaticResource Wd.Button.Ghost}"
Click="OnSettingsToggleClick"
Padding="6,4"
ToolTip="Settings">
<Path Data="M 8,1 L 8,3 M 8,13 L 8,15 M 1,8 L 3,8 M 13,8 L 15,8 M 3,3 L 4.5,4.5 M 11.5,11.5 L 13,13 M 3,13 L 4.5,11.5 M 11.5,4.5 L 13,3 M 8,5.5 C 9.4,5.5 10.5,6.6 10.5,8 C 10.5,9.4 9.4,10.5 8,10.5 C 6.6,10.5 5.5,9.4 5.5,8 C 5.5,6.6 6.6,5.5 8,5.5"
Stroke="{DynamicResource Wd.Text.Secondary}"
StrokeThickness="1.4"
Fill="Transparent"
Width="14" Height="14"
Stretch="None"/>
</Button>
</StackPanel>
</Grid>
</Border>
<!-- ═════════════════════ TRANSPORT STRIP (40px) ═════════════════════ -->
<Border Grid.Row="1"
Background="{DynamicResource Wd.Canvas}"
BorderBrush="{DynamicResource Wd.Border}"
BorderThickness="0,0,0,1">
<Grid Margin="20,0,20,0" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal"
VerticalAlignment="Center">
<!-- Session timer — green dot + elapsed when at least one ISO is live -->
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center"
Margin="0,0,20,0"
Visibility="{Binding IsSessionActive, Converter={StaticResource BoolToVis}}">
<Ellipse Width="7" Height="7"
Fill="{DynamicResource Wd.Accent.Cyan}"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding SessionElapsed}"
FontFamily="{StaticResource Wd.Font.Mono}"
FontSize="12"
FontWeight="Medium"
Foreground="{DynamicResource Wd.Text.Primary}"
VerticalAlignment="Center"
Margin="8,0,0,0"/>
</StackPanel>
<!-- Participant counts — labels in tracked smallcaps, values in mono.
The space between value and next label uses a middle dot. -->
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center"
Margin="0,0,20,0">
<TextBlock Text="PART"
FontFamily="{StaticResource Wd.Font.Sans}"
FontSize="10"
FontWeight="SemiBold"
Foreground="{DynamicResource Wd.Text.Tertiary}"
VerticalAlignment="Center"
Margin="0,0,6,0"/>
<TextBlock Text="{Binding ParticipantCount, Mode=OneWay, FallbackValue=0}"
FontFamily="{StaticResource Wd.Font.Mono}"
FontSize="12"
Foreground="{DynamicResource Wd.Text.Primary}"
VerticalAlignment="Center"/>
<TextBlock Text=" · "
FontFamily="{StaticResource Wd.Font.Mono}"
FontSize="12"
Foreground="{DynamicResource Wd.Text.Disabled}"
VerticalAlignment="Center"/>
<TextBlock Text="LIVE"
FontFamily="{StaticResource Wd.Font.Sans}"
FontSize="10"
FontWeight="SemiBold"
Foreground="{DynamicResource Wd.Text.Tertiary}"
VerticalAlignment="Center"
Margin="0,0,6,0"/>
<TextBlock Text="{Binding LiveCount, Mode=OneWay, FallbackValue=0}"
FontFamily="{StaticResource Wd.Font.Mono}"
FontSize="12"
Foreground="{DynamicResource Wd.Accent.CyanText}"
VerticalAlignment="Center"/>
</StackPanel>
<!-- Teams meeting pill — only present in-call -->
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center"
Margin="0,0,20,0"
Visibility="{Binding IsTeamsInCall, Converter={StaticResource BoolToVis}}">
<TextBlock Text="CALL"
FontFamily="{StaticResource Wd.Font.Sans}"
FontSize="10"
FontWeight="SemiBold"
Foreground="{DynamicResource Wd.Accent.CyanText}"
VerticalAlignment="Center"
Margin="0,0,6,0"/>
<TextBlock Text="{Binding TeamsMeetingState}"
FontFamily="{StaticResource Wd.Font.Mono}"
FontSize="11"
Foreground="{DynamicResource Wd.Text.Secondary}"
VerticalAlignment="Center"
MaxWidth="240"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</StackPanel>
<!-- Control surface — only present when listening, right-aligned -->
<StackPanel Grid.Column="2"
Orientation="Horizontal"
VerticalAlignment="Center"
Visibility="{Binding IsControlSurfaceRunning, Converter={StaticResource BoolToVis}}">
<TextBlock Text="CTRL"
FontFamily="{StaticResource Wd.Font.Sans}"
FontSize="10"
FontWeight="SemiBold"
Foreground="{DynamicResource Wd.Text.Tertiary}"
VerticalAlignment="Center"
Margin="0,0,6,0"/>
<TextBlock Text="{Binding ControlSurfaceText}"
FontFamily="{StaticResource Wd.Font.Mono}"
FontSize="11"
Foreground="{DynamicResource Wd.Accent.CyanText}"
VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</Border>
<!-- ═════════════════════ BODY ═════════════════════ -->
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Alert banner — appears when AlertBanner.IsVisible flips on -->
<Border Grid.Row="0"
Background="{DynamicResource Wd.Accent.CoralBg}"
BorderBrush="{DynamicResource Wd.Accent.Coral}"
BorderThickness="0,0,0,1"
Padding="24,10"
Visibility="{Binding AlertBanner.IsVisible, Converter={StaticResource BoolToVis}}">
<DockPanel>
<Button DockPanel.Dock="Right"
Style="{StaticResource Wd.Button.Ghost}"
Content="Dismiss"
Command="{Binding AlertBanner.DismissCommand}"
Margin="12,0,0,0"/>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse Width="7" Height="7"
Fill="{DynamicResource Wd.Accent.Coral}"
VerticalAlignment="Center"
Margin="0,0,10,0"/>
<TextBlock Text="{Binding AlertBanner.Message}"
Style="{StaticResource Wd.Text.Body}"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
</StackPanel>
</DockPanel>
</Border>
<!-- Update banner — Forgejo release available -->
<Border Grid.Row="1"
Margin="20,12,20,0"
Padding="14,10"
Background="{DynamicResource Wd.Accent.CyanMuted}"
BorderBrush="{DynamicResource Wd.Accent.Cyan}"
BorderThickness="1"
CornerRadius="{StaticResource Radius.M}"
Visibility="{Binding UpdateBanner.IsVisible, Converter={StaticResource BoolToVis}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0"
Width="7" Height="7"
Fill="{DynamicResource Wd.Accent.Cyan}"
VerticalAlignment="Center"
Margin="0,0,12,0"/>
<TextBlock Grid.Column="1"
Text="{Binding UpdateBanner.Message}"
Style="{StaticResource Wd.Text.Body}"
FontSize="12"
VerticalAlignment="Center"/>
<Button Grid.Column="2"
Style="{StaticResource Wd.Button.Ghost}"
Content="Get update"
Command="{Binding UpdateBanner.OpenReleasePageCommand}"
Padding="14,4"
Margin="0,0,8,0"/>
<Button Grid.Column="3"
Style="{StaticResource Wd.Button.Ghost}"
Content="Dismiss"
Command="{Binding UpdateBanner.DismissCommand}"
Padding="14,4"/>
</Grid>
</Border>
<!-- Action toolbar — primary verbs above the participants table -->
<Border Grid.Row="2"
Padding="20,14,20,12">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Button Style="{StaticResource Wd.Button.Primary}"
Command="{Binding EnableAllOnlineCommand}"
Content="Enable all"
Padding="14,7"
Margin="0,0,8,0"
ToolTip="Enable ISO routing for every online participant"/>
<Button Style="{StaticResource Wd.Button.Ghost}"
Command="{Binding RefreshDiscoveryCommand}"
Content="Refresh"
Padding="14,7"
Margin="0,0,8,0"
ToolTip="Refresh NDI discovery (Ctrl+R)"/>
<Button Style="{StaticResource Wd.Button.Ghost}"
Click="OnPresetsClick"
Content="Presets"
Padding="14,7"
Margin="0,0,8,0"
ToolTip="Open operator presets"/>
<Button Style="{StaticResource Wd.Button.Ghost}"
Command="{Binding StopAllIsosCommand}"
Content="Stop all"
Padding="14,7"
ToolTip="Disable every running ISO (Ctrl+Shift+S)"/>
<!-- Teams launch / hide — small icon buttons, after a separator -->
<Border Width="1"
Background="{DynamicResource Wd.Border}"
Margin="14,4,14,4"/>
<Button Style="{StaticResource Wd.Button.Ghost}"
Click="OnLaunchTeamsClick"
MouseRightButtonUp="OnLaunchTeamsRightClick"
Padding="10,7"
Margin="0,0,4,0"
ToolTip="Launch Microsoft Teams (right-click to stop)">
<Path Data="M 4,8 L 16,8 L 16,16 L 4,16 Z M 16,11 L 22,8 L 22,16 L 16,13 Z"
Stroke="{DynamicResource Wd.Text.Secondary}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
Width="18" Height="14"
Stretch="Uniform"/>
</Button>
<Button Style="{StaticResource Wd.Button.Ghost}"
Click="OnToggleTeamsWindowClick"
Padding="10,7"
ToolTip="Hide / show Teams windows">
<Path Data="M 1,11 C 5,4 17,4 21,11 C 17,18 5,18 1,11 Z M 11,8 A 3,3 0 1 1 11,14 A 3,3 0 1 1 11,8 Z"
Stroke="{DynamicResource Wd.Text.Secondary}"
StrokeThickness="1.4"
Fill="Transparent"
Width="18" Height="14"
Stretch="Uniform"/>
</Button>
</StackPanel>
</Border>
<!-- Participants table — DataGrid placeholder pending task 39's redesign.
For v2's first cut the existing column structure is kept so the
shell rebuild doesn't also change row semantics. The 5-column
"Studio Terminal" table comes in the next commit. -->
<Border Grid.Row="3"
Margin="20,0,20,12"
BorderBrush="{DynamicResource Wd.Border}"
BorderThickness="1"
CornerRadius="{StaticResource Radius.M}"
Background="{DynamicResource Wd.Surface}">
<DataGrid x:Name="ParticipantsGrid"
ItemsSource="{Binding ParticipantsView}"
AutoGenerateColumns="False"
HeadersVisibility="Column"
GridLinesVisibility="None"
RowBackground="Transparent"
AlternatingRowBackground="Transparent"
BorderThickness="0"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserResizeRows="False"
SelectionMode="Single"
SelectionUnit="FullRow"
RowHeight="56">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding DisplayName}"
Width="*"
IsReadOnly="True"/>
<DataGridTextColumn Header="Source"
Binding="{Binding SourceFullName}"
Width="*"
IsReadOnly="True"/>
<DataGridTemplateColumn Header="ISO" Width="120">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Command="{Binding ToggleIsoCommand}"
Margin="0,0,12,0"
Padding="14,6">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource Wd.Button.IsoToggle}">
<Setter Property="Content" Value="Enable"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled}" Value="True">
<Setter Property="Content" Value="● LIVE"/>
<Setter Property="Background" Value="{DynamicResource Wd.Accent.CyanMuted}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Wd.Accent.Cyan}"/>
<Setter Property="Foreground" Value="{DynamicResource Wd.Accent.CyanText}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Border>
</Grid>
<!-- ═════════════════════ MEETING BAR (conditional) ═════════════════════ -->
<Border Grid.Row="3"
Background="{DynamicResource Wd.SurfaceElevated}"
BorderBrush="{DynamicResource Wd.Border}"
BorderThickness="0,1,0,0"
Padding="20,10,20,10"
Visibility="{Binding IsTeamsInCall, Converter={StaticResource BoolToVis}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock Text="IN CALL"
FontFamily="{StaticResource Wd.Font.Sans}"
FontSize="10"
FontWeight="SemiBold"
Foreground="{DynamicResource Wd.Accent.CyanText}"
VerticalAlignment="Center"
Margin="0,0,10,0"/>
<TextBlock Text="{Binding TeamsMeetingState}"
FontFamily="{StaticResource Wd.Font.Mono}"
FontSize="12"
Foreground="{DynamicResource Wd.Text.Primary}"
VerticalAlignment="Center"
MaxWidth="500"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
<!-- Quick-join field — paste a meeting URL, click Join -->
<StackPanel Grid.Column="1"
Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="{Binding IsTeamsInCall, Converter={StaticResource BoolToVisInverse}}">
<!-- Hidden when in call to keep the bar uncluttered -->
</StackPanel>
<StackPanel Grid.Column="2"
Orientation="Horizontal"
VerticalAlignment="Center">
<Button Style="{StaticResource Wd.Button.Ghost}"
Command="{Binding ToggleMuteCommand}"
Content="Mute"
Padding="14,6"
Margin="0,0,6,0"
MinWidth="64"
ToolTip="Toggle microphone mute in Teams"/>
<Button Style="{StaticResource Wd.Button.Ghost}"
Command="{Binding ToggleCameraCommand}"
Content="Cam"
Padding="14,6"
Margin="0,0,6,0"
MinWidth="64"
ToolTip="Toggle camera in Teams"/>
<Button Style="{StaticResource Wd.Button.Ghost}"
Command="{Binding LeaveCallCommand}"
Content="Leave"
Padding="14,6"
MinWidth="64"
Foreground="{DynamicResource Wd.Accent.Coral}"
BorderBrush="{DynamicResource Wd.Accent.Coral}"
ToolTip="Leave the Teams call"/>
</StackPanel>
</Grid>
</Border>
<!-- ═════════════════════ SETTINGS DRAWER (overlay) ═════════════════════
Slides in over the main content from the right when the header gear
is clicked. Backdrop scrim is a semi-transparent overlay over the
whole window; clicking it dismisses the drawer. Esc also dismisses
(handled in code-behind). The drawer carries the same settings
surface as v1 — OUTPUT / NETWORK / DISPLAY tabs with the existing
bindings — so the engine wiring is preserved through the shell
rebuild. -->
<Grid Grid.Row="0"
Grid.RowSpan="4"
x:Name="SettingsDrawerOverlay"
Visibility="Collapsed">
<!-- Scrim — click to dismiss -->
<Border Background="#80000000"
MouseLeftButtonDown="OnSettingsScrimClick"/>
<!-- Drawer panel -->
<Border HorizontalAlignment="Right"
Width="420"
Background="{DynamicResource Wd.Surface}"
BorderBrush="{DynamicResource Wd.Border}"
BorderThickness="1,0,0,0">
<DockPanel LastChildFill="True">
<!-- Drawer header -->
<Border DockPanel.Dock="Top"
Padding="20,18,16,16"
BorderBrush="{DynamicResource Wd.Border}"
BorderThickness="0,0,0,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Settings"
Style="{StaticResource Wd.Text.Title}"
FontSize="18"/>
<TextBlock Text="Per-session global processing"
Style="{StaticResource Wd.Text.Subtle}"
Foreground="{DynamicResource Wd.Text.Tertiary}"
Margin="0,2,0,0"/>
</StackPanel>
<Button Grid.Column="1"
Style="{StaticResource Wd.Button.Ghost}"
Click="OnSettingsToggleClick"
Padding="8,4"
ToolTip="Close (Esc)">
<Path Data="M 0,0 L 10,10 M 10,0 L 0,10"
Stroke="{DynamicResource Wd.Text.Secondary}"
StrokeThickness="1.4"
Width="10" Height="10"
Stretch="None"/>
</Button>
</Grid>
</Border>
<!-- Drawer footer (Apply) -->
<Border DockPanel.Dock="Bottom"
Padding="20,12,20,16"
BorderBrush="{DynamicResource Wd.Border}"
BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Style="{StaticResource Wd.Button.Ghost}"
Content="Close"
Click="OnSettingsToggleClick"
Padding="14,6"
Margin="0,0,8,0"/>
<Button Style="{StaticResource Wd.Button.Primary}"
Content="Apply changes"
Command="{Binding Settings.ApplyCommand}"
Padding="14,6"/>
</StackPanel>
</Border>
<!-- Drawer scrollable body — three tabs -->
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<Border Padding="20,16,20,16">
<TabControl Style="{StaticResource Wd.TabControl}">
<!-- ─────── OUTPUT ─────── -->
<TabItem Header="OUTPUT" Style="{StaticResource Wd.TabItem}">
<StackPanel>
<TextBlock Text="Target framerate"
Style="{StaticResource Wd.Text.Subtle}"
Margin="0,0,0,4"/>
<ComboBox ItemsSource="{Binding Settings.AvailableFramerates}"
SelectedItem="{Binding Settings.Framerate}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource EnumDesc}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="Target resolution"
Style="{StaticResource Wd.Text.Subtle}"
Margin="0,12,0,4"/>
<ComboBox ItemsSource="{Binding Settings.AvailableResolutions}"
SelectedItem="{Binding Settings.Resolution}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource EnumDesc}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="Aspect mode"
Style="{StaticResource Wd.Text.Subtle}"
Margin="0,12,0,4"/>
<ComboBox ItemsSource="{Binding Settings.AvailableAspectModes}"
SelectedItem="{Binding Settings.Aspect}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource EnumDesc}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="Audio mode"
Style="{StaticResource Wd.Text.Subtle}"
Margin="0,12,0,4"/>
<ComboBox ItemsSource="{Binding Settings.AvailableAudioModes}"
SelectedItem="{Binding Settings.Audio}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource EnumDesc}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Style="{StaticResource Wd.Button.Ghost}"
Content="Reset to defaults"
Command="{Binding Settings.ResetOutputDefaultsCommand}"
HorizontalAlignment="Stretch"
Margin="0,16,0,0"
Padding="0,8"/>
</StackPanel>
</TabItem>
<!-- ─────── NETWORK ─────── -->
<TabItem Header="NETWORK" Style="{StaticResource Wd.TabItem}">
<StackPanel>
<TextBlock Text="Discovery group"
Style="{StaticResource Wd.Text.Subtle}"
Margin="0,0,0,4"/>
<TextBox Text="{Binding Settings.DiscoveryGroups, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Output group"
Style="{StaticResource Wd.Text.Subtle}"
Margin="0,12,0,4"/>
<TextBox Text="{Binding Settings.OutputGroups, UpdateSourceTrigger=PropertyChanged}"/>
<Button Style="{StaticResource Wd.Button.Ghost}"
Content="Apply transcoder topology"
Command="{Binding Settings.ApplyTranscoderTopologyCommand}"
HorizontalAlignment="Stretch"
Margin="0,16,0,0"
Padding="0,9"/>
<TextBlock Text="Pins Teams' raw NDI broadcasts to a private 'teamsiso-input' group so they don't pollute the Public network. Restart Teams after applying."
Style="{StaticResource Wd.Text.Body}"
FontSize="11"
Foreground="{DynamicResource Wd.Text.Tertiary}"
TextWrapping="Wrap"
Margin="0,6,0,0"/>
<TextBlock Text="Output name template"
Style="{StaticResource Wd.Text.Subtle}"
Margin="0,16,0,4"/>
<TextBox Text="{Binding Settings.OutputNameTemplate, UpdateSourceTrigger=LostFocus}"
FontFamily="{StaticResource Wd.Font.Mono}"
FontSize="11"/>
<TextBlock Text="Tokens: {name}, {guid}, {machine}, {timestamp}. Default: TEAMSISO_{guid}."
Style="{StaticResource Wd.Text.Body}"
FontSize="11"
Foreground="{DynamicResource Wd.Text.Tertiary}"
TextWrapping="Wrap"
Margin="0,4,0,0"/>
</StackPanel>
</TabItem>
<!-- ─────── APP ─────── -->
<TabItem Header="APP" Style="{StaticResource Wd.TabItem}">
<StackPanel>
<CheckBox Content="Hide my self-preview from participants"
IsChecked="{Binding Settings.HideLocalSelf}"/>
<CheckBox Content="Auto-disable ISOs when a participant leaves"
IsChecked="{Binding Settings.AutoDisableOnDeparture}"
Margin="0,8,0,0"/>
<CheckBox Content="Auto-apply last preset on launch"
IsChecked="{Binding Settings.AutoApplyLastPreset}"
Margin="0,8,0,0"/>
<TextBlock Text="Sort participants by"
Style="{StaticResource Wd.Text.Subtle}"
Margin="0,14,0,4"/>
<ComboBox ItemsSource="{Binding Settings.AvailableSortModes}"
SelectedItem="{Binding Settings.ParticipantSort}"/>
<CheckBox Content="Minimize to system tray"
IsChecked="{Binding Settings.MinimizeToTray}"
Margin="0,14,0,0"/>
<Border Margin="0,16,0,8"
Height="1"
Background="{DynamicResource Wd.Border}"/>
<CheckBox Content="Launch Microsoft Teams on TeamsISO startup"
IsChecked="{Binding Settings.LaunchTeamsOnStartup}"/>
<CheckBox Content="Auto-hide Teams windows when launched"
IsChecked="{Binding Settings.AutoHideTeamsWindows}"
Margin="0,8,0,0"/>
<Border Margin="0,16,0,8"
Height="1"
Background="{DynamicResource Wd.Border}"/>
<CheckBox Content="Embed Teams window (experimental)"
IsChecked="{Binding Settings.EmbedTeamsWindow}"/>
<Button Style="{StaticResource Wd.Button.Ghost}"
Content="Open embed window"
Click="OnOpenEmbedWindowClick"
HorizontalAlignment="Stretch"
Margin="0,8,0,0"
Padding="0,8"
IsEnabled="{Binding Settings.EmbedTeamsWindow}"/>
<Border Margin="0,16,0,8"
Height="1"
Background="{DynamicResource Wd.Border}"/>
<CheckBox Content="Control surface (Stream Deck / Companion / web)"
IsChecked="{Binding Settings.ControlSurfaceEnabled}"/>
<CheckBox Content="LAN-reachable"
IsChecked="{Binding Settings.ControlSurfaceLanReachable}"
Margin="20,4,0,0"/>
<TextBlock Text="{Binding Settings.ControlSurfaceUrl}"
Style="{StaticResource Wd.Text.Mono}"
FontSize="11"
Foreground="{DynamicResource Wd.Accent.CyanText}"
Margin="20,4,0,0"/>
</StackPanel>
</TabItem>
</TabControl>
</Border>
</ScrollViewer>
</DockPanel>
</Border>
</Grid>
</Grid>
</Window>