Some checks failed
CI / build-and-test (push) Failing after 28s
Reparents Teams' main top-level window into a TeamsISO-owned host via Win32 SetParent + window-style stripping. Operator gets Teams visually INSIDE TeamsISO instead of as a separate window — completes the 'Teams runs within this app' direction the user asked for after auto-hide. Strictly opt-in (DISPLAY tab → 'Embed Teams window (experimental)'). Modern Teams runs WebView2 in its main window; WebView2 is sensitive to parent changes and may render glitches or refuse focus. If so, operator unticks and falls back to auto-hide mode. Implementation: - TeamsLauncher.EmbedTeamsInto(hostHwnd, w, h): finds Teams' main window (longest-title heuristic — same as GetActiveWindowTitle), saves original parent + WS_STYLE, SetParents into host, strips WS_CAPTION + WS_THICKFRAME + WS_BORDER + WS_DLGFRAME + WS_POPUP, adds WS_CHILD, MoveWindow to fit. - TeamsLauncher.RestoreEmbed(): SetParent back to desktop + restore saved window styles. Idempotent — safe to call on shutdown even if nothing was embedded. - TeamsLauncher.ResizeEmbedded(w, h): MoveWindow to new dimensions; called from host SizeChanged event. - New TeamsEmbedWindow chromeless host with an EXPERIMENTAL pill in the caption. Loaded → grab HwndSource from EmbedHost Border → call EmbedTeamsInto. SizeChanged → ResizeEmbedded. Closed → RestoreEmbed (in try/finally so a crash can't leave Teams orphaned). Friendly fallback messages if no Teams window exists or HWND grab fails. - Settings → DISPLAY → checkbox + 'Open embed window' button (gated by the checkbox). Persisted via EmbedTeamsWindow on UIPreferences.
1492 lines
99 KiB
XML
1492 lines
99 KiB
XML
<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:vm="clr-namespace:TeamsISO.App.ViewModels"
|
||
xmlns:conv="clr-namespace:TeamsISO.App.Converters"
|
||
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
||
Title="TeamsISO"
|
||
Icon="/Assets/teamsiso.ico"
|
||
Height="780" Width="1280"
|
||
MinHeight="640" MinWidth="1080"
|
||
Background="{DynamicResource Wd.Canvas}"
|
||
WindowStyle="None"
|
||
ResizeMode="CanResize"
|
||
UseLayoutRounding="True"
|
||
TextOptions.TextFormattingMode="Ideal"
|
||
TextOptions.TextRenderingMode="ClearType">
|
||
|
||
<!--
|
||
Chromeless window chrome — removes the standard Windows title bar so we can
|
||
draw our own header that matches the Teams flush look. CaptionHeight=44 makes
|
||
the top 44px of the window act as a drag region. ResizeBorderThickness=6
|
||
keeps edge-resize working. UseAeroCaptionButtons=False disables the OS-drawn
|
||
min/max/close (we draw our own).
|
||
-->
|
||
<shell:WindowChrome.WindowChrome>
|
||
<shell:WindowChrome
|
||
CaptionHeight="44"
|
||
ResizeBorderThickness="6"
|
||
CornerRadius="0"
|
||
GlassFrameThickness="0"
|
||
UseAeroCaptionButtons="False"/>
|
||
</shell:WindowChrome.WindowChrome>
|
||
|
||
<Window.Resources>
|
||
<conv:BoolToVisibilityConverter x:Key="BoolToVis"/>
|
||
<conv:BoolToVisibilityConverter x:Key="InvertBool"
|
||
TrueValue="Collapsed"
|
||
FalseValue="Visible"/>
|
||
<conv:EnumDescriptionConverter x:Key="EnumDesc"/>
|
||
<conv:InitialsConverter x:Key="Initials"/>
|
||
<conv:CountToVisibilityConverter x:Key="CountToVis"/>
|
||
</Window.Resources>
|
||
|
||
<!--
|
||
Window-scoped keyboard shortcuts. Bound to view-model commands where
|
||
available; F1 routes to a code-behind handler that opens HelpWindow
|
||
(the help dialog is a host concern, not a VM concern). Window-scoped
|
||
means these only fire when TeamsISO is the foreground window — using
|
||
RegisterHotKey for global shortcuts would be a v2 concern (operators
|
||
rarely want a Stream Deck and a global hotkey for the same action).
|
||
-->
|
||
<Window.InputBindings>
|
||
<KeyBinding Key="F1" Command="{Binding ShowHelpCommand}"/>
|
||
<KeyBinding Key="M" Modifiers="Ctrl" Command="{Binding DropRecordingMarkerCommand}"/>
|
||
<KeyBinding Key="S" Modifiers="Ctrl+Shift" Command="{Binding StopAllIsosCommand}"/>
|
||
<KeyBinding Key="R" Modifiers="Ctrl" Command="{Binding RefreshDiscoveryCommand}"/>
|
||
</Window.InputBindings>
|
||
|
||
<Grid>
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="72"/> <!-- Left rail -->
|
||
<ColumnDefinition Width="*"/> <!-- Main content -->
|
||
<ColumnDefinition Width="380"/> <!-- Settings panel -->
|
||
</Grid.ColumnDefinitions>
|
||
|
||
<!-- ════════════════════════════════════════════════════════════════
|
||
LEFT RAIL (Teams-style nav)
|
||
════════════════════════════════════════════════════════════════ -->
|
||
<Border Grid.Column="0"
|
||
Background="{DynamicResource Wd.Rail}"
|
||
BorderBrush="{DynamicResource Wd.Border}"
|
||
BorderThickness="0,0,1,0">
|
||
<DockPanel LastChildFill="False">
|
||
|
||
<!-- Wild Dragon mark — real logo from wilddragon.net. Clickable: opens About.
|
||
Lives inside the chromeless title bar's drag region, so we opt into
|
||
hit-testing so clicks land on the button rather than starting a drag. -->
|
||
<Button DockPanel.Dock="Top"
|
||
Style="{StaticResource Wd.Button.RailIcon}"
|
||
Width="48" Height="56"
|
||
Margin="0,12,0,4"
|
||
Click="OnAboutClick"
|
||
shell:WindowChrome.IsHitTestVisibleInChrome="True"
|
||
ToolTip="About TeamsISO">
|
||
<StackPanel>
|
||
<Image Source="/Assets/dragon-mark.png"
|
||
Width="32" Height="32"
|
||
HorizontalAlignment="Center"
|
||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
||
</StackPanel>
|
||
</Button>
|
||
|
||
<TextBlock DockPanel.Dock="Top"
|
||
Text="Wild Dragon"
|
||
Style="{StaticResource Wd.Text.Caption}"
|
||
Foreground="{DynamicResource Wd.Accent.Cyan}"
|
||
HorizontalAlignment="Center"
|
||
FontSize="9"
|
||
Margin="0,0,0,12"/>
|
||
|
||
<!-- Divider -->
|
||
<Border DockPanel.Dock="Top"
|
||
Height="1"
|
||
Background="{DynamicResource Wd.Border}"
|
||
Margin="14,0,14,12"/>
|
||
|
||
<!-- Nav: Participants (active) -->
|
||
<Button DockPanel.Dock="Top"
|
||
Style="{StaticResource Wd.Button.RailIcon}"
|
||
ToolTip="Participants">
|
||
<Grid>
|
||
<Border Width="48" Height="48"
|
||
CornerRadius="8"
|
||
Background="{DynamicResource Wd.Accent.CyanMuted}"/>
|
||
<Path Data="M 8,17 C 8,13 12,11 14,11 C 16,11 20,13 20,17 M 14,5 C 16,5 17.5,6.5 17.5,8.5 C 17.5,10.5 16,12 14,12 C 12,12 10.5,10.5 10.5,8.5 C 10.5,6.5 12,5 14,5"
|
||
Stroke="{DynamicResource Wd.Accent.Cyan}"
|
||
StrokeThickness="1.6"
|
||
Fill="Transparent"
|
||
HorizontalAlignment="Center"
|
||
VerticalAlignment="Center"
|
||
Width="28" Height="22"
|
||
Stretch="Uniform"/>
|
||
</Grid>
|
||
</Button>
|
||
|
||
<!-- Nav: Launch Teams. Click = launch / surface; right-click = stop.
|
||
The previous "left-click toggles" behavior ambushed operators
|
||
who'd hidden Teams' windows via the eye toggle next door —
|
||
they'd hit Launch expecting Teams to come back and instead
|
||
get a "Close all Teams windows now?" dialog. Now click is
|
||
always idempotent-progressive (running but hidden → show;
|
||
not running → launch). Right-click for stop. -->
|
||
<Button DockPanel.Dock="Top"
|
||
Style="{StaticResource Wd.Button.RailIcon}"
|
||
Click="OnLaunchTeamsClick"
|
||
MouseRightButtonUp="OnLaunchTeamsRightClick"
|
||
ToolTip="Launch Microsoft Teams (or surface its window). Right-click to stop Teams.">
|
||
<!-- Stylized 'video meeting' camera icon -->
|
||
<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.6"
|
||
Fill="Transparent"
|
||
StrokeLineJoin="Round"
|
||
Width="22" Height="20"
|
||
Stretch="Uniform"/>
|
||
</Button>
|
||
|
||
<!-- Nav: Hide / Show Teams windows. Phase E.2 of the embedded-Teams roadmap.
|
||
Toggles every visible top-level Teams window between SW_HIDE and SW_SHOW
|
||
so the operator can keep TeamsISO foregrounded without alt-tabbing. -->
|
||
<Button DockPanel.Dock="Top"
|
||
Style="{StaticResource Wd.Button.RailIcon}"
|
||
Click="OnToggleTeamsWindowClick"
|
||
ToolTip="Hide / show Microsoft Teams windows (keeps Teams running but moves it out of the way)">
|
||
<!-- Eye icon (open / closed handled by the click toggling Teams visibility, not the icon) -->
|
||
<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="22" Height="22"
|
||
Stretch="Uniform"/>
|
||
</Button>
|
||
|
||
<!-- Nav: Settings (placeholder — opens panel on right; not toggled in this build) -->
|
||
<Button DockPanel.Dock="Top"
|
||
Style="{StaticResource Wd.Button.RailIcon}"
|
||
ToolTip="Settings">
|
||
<Path Data="M 14,4 L 14,7 M 14,21 L 14,24 M 4,14 L 7,14 M 21,14 L 24,14 M 7.5,7.5 L 9.5,9.5 M 18.5,18.5 L 20.5,20.5 M 7.5,20.5 L 9.5,18.5 M 18.5,9.5 L 20.5,7.5 M 14,11 C 15.7,11 17,12.3 17,14 C 17,15.7 15.7,17 14,17 C 12.3,17 11,15.7 11,14 C 11,12.3 12.3,11 14,11"
|
||
Stroke="{DynamicResource Wd.Text.Secondary}"
|
||
StrokeThickness="1.5"
|
||
Fill="Transparent"
|
||
Width="22" Height="22"
|
||
Stretch="Uniform"/>
|
||
</Button>
|
||
|
||
<!-- Engine status indicator at bottom -->
|
||
<Border DockPanel.Dock="Bottom"
|
||
Width="40" Height="40"
|
||
Margin="0,0,0,16"
|
||
HorizontalAlignment="Center"
|
||
Background="{DynamicResource Wd.Status.LiveBg}"
|
||
CornerRadius="20"
|
||
ToolTip="{Binding StatusText}">
|
||
<Ellipse Width="10" Height="10"
|
||
Fill="{DynamicResource Wd.Status.Live}"
|
||
HorizontalAlignment="Center"
|
||
VerticalAlignment="Center"/>
|
||
</Border>
|
||
</DockPanel>
|
||
</Border>
|
||
|
||
<!-- ════════════════════════════════════════════════════════════════
|
||
MAIN CONTENT
|
||
════════════════════════════════════════════════════════════════ -->
|
||
<DockPanel Grid.Column="1" LastChildFill="True">
|
||
|
||
<!-- Alert banner -->
|
||
<Border DockPanel.Dock="Top"
|
||
Background="{DynamicResource Wd.Accent.CoralBg}"
|
||
BorderBrush="{DynamicResource Wd.Accent.Coral}"
|
||
BorderThickness="0,0,0,1"
|
||
Padding="24,12"
|
||
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="8" Height="8"
|
||
Fill="{DynamicResource Wd.Accent.Coral}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,12,0"/>
|
||
<TextBlock Text="{Binding AlertBanner.Message}"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontWeight="SemiBold"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</DockPanel>
|
||
</Border>
|
||
|
||
<!-- Header strip -->
|
||
<Border DockPanel.Dock="Top"
|
||
BorderBrush="{DynamicResource Wd.Border}"
|
||
BorderThickness="0,0,0,1"
|
||
Padding="32,20">
|
||
<Grid>
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="*"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
</Grid.ColumnDefinitions>
|
||
|
||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||
<TextBlock Text="TeamsISO"
|
||
Style="{StaticResource Wd.Text.Title}"
|
||
VerticalAlignment="Center"/>
|
||
<Border Style="{StaticResource Wd.Pill}"
|
||
Margin="14,0,0,0"
|
||
VerticalAlignment="Center">
|
||
<TextBlock Text="by Wild Dragon"
|
||
Style="{StaticResource Wd.Text.Mono}"
|
||
FontSize="11"
|
||
Foreground="{DynamicResource Wd.Accent.Cyan}"/>
|
||
</Border>
|
||
</StackPanel>
|
||
|
||
<StackPanel Grid.Column="1"
|
||
Orientation="Horizontal"
|
||
VerticalAlignment="Center">
|
||
<Border Style="{StaticResource Wd.Pill}"
|
||
Background="{DynamicResource Wd.Status.LiveBg}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,16,0">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Ellipse Width="7" Height="7"
|
||
Fill="{DynamicResource Wd.Status.Live}"
|
||
VerticalAlignment="Center"/>
|
||
<TextBlock Text="{Binding StatusText}"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
Margin="8,0,0,0"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!--
|
||
Custom window control buttons. WindowChrome.IsHitTestVisibleInChrome
|
||
tells the chrome that clicks here are real clicks, not drag-region
|
||
clicks, so the buttons fire instead of starting a window drag.
|
||
-->
|
||
<Button x:Name="MinimizeButton"
|
||
Style="{StaticResource Wd.Button.Caption}"
|
||
Click="OnMinimize"
|
||
shell:WindowChrome.IsHitTestVisibleInChrome="True"
|
||
ToolTip="Minimize">
|
||
<Path Data="M 0,5 L 10,5"
|
||
Stroke="{DynamicResource Wd.Text.Primary}"
|
||
StrokeThickness="1.2"
|
||
Width="10" Height="10"
|
||
Stretch="None"/>
|
||
</Button>
|
||
<Button x:Name="MaximizeButton"
|
||
Style="{StaticResource Wd.Button.Caption}"
|
||
Click="OnMaximizeRestore"
|
||
shell:WindowChrome.IsHitTestVisibleInChrome="True"
|
||
ToolTip="Maximize">
|
||
<Path x:Name="MaximizeIcon"
|
||
Data="M 0,0 L 10,0 L 10,10 L 0,10 Z"
|
||
Stroke="{DynamicResource Wd.Text.Primary}"
|
||
StrokeThickness="1.2"
|
||
Fill="Transparent"
|
||
Width="10" Height="10"
|
||
Stretch="None"/>
|
||
</Button>
|
||
<Button x:Name="CloseButton"
|
||
Style="{StaticResource Wd.Button.CaptionClose}"
|
||
Click="OnClose"
|
||
shell:WindowChrome.IsHitTestVisibleInChrome="True"
|
||
ToolTip="Close">
|
||
<Path Data="M 0,0 L 10,10 M 10,0 L 0,10"
|
||
Stroke="{DynamicResource Wd.Text.Primary}"
|
||
StrokeThickness="1.2"
|
||
Width="10" Height="10"
|
||
Stretch="None"/>
|
||
</Button>
|
||
</StackPanel>
|
||
</Grid>
|
||
</Border>
|
||
|
||
<!-- Footer -->
|
||
<Border DockPanel.Dock="Bottom"
|
||
BorderBrush="{DynamicResource Wd.Border}"
|
||
BorderThickness="0,1,0,0"
|
||
Padding="32,10">
|
||
<Grid>
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="*"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
</Grid.ColumnDefinitions>
|
||
<TextBlock Text="WOOGLIN"
|
||
Style="{StaticResource Wd.Text.Mono}"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
VerticalAlignment="Center"/>
|
||
<TextBlock Grid.Column="1"
|
||
Text="{Binding StatusText}"
|
||
Style="{StaticResource Wd.Text.Mono}"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
HorizontalAlignment="Center"
|
||
VerticalAlignment="Center"/>
|
||
|
||
<!-- Session timer — green dot + elapsed when at least one ISO is live -->
|
||
<StackPanel Grid.Column="2"
|
||
Orientation="Horizontal"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,16,0"
|
||
Visibility="{Binding IsSessionActive, Converter={StaticResource BoolToVis}}"
|
||
ToolTip="Elapsed time since the first ISO went live this session.">
|
||
<Border Width="8" Height="8"
|
||
CornerRadius="4"
|
||
Background="{DynamicResource Wd.Status.Live}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,6,0"/>
|
||
<TextBlock Text="{Binding SessionElapsed}"
|
||
Style="{StaticResource Wd.Text.Mono}"
|
||
FontSize="11"
|
||
Foreground="{DynamicResource Wd.Status.Live}"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
|
||
<!-- Recording badge — coral dot + count when recording is on -->
|
||
<StackPanel Grid.Column="3"
|
||
Orientation="Horizontal"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,16,0"
|
||
Visibility="{Binding IsRecording, Converter={StaticResource BoolToVis}}"
|
||
ToolTip="One or more ISOs are being recorded to disk.">
|
||
<Border Width="8" Height="8"
|
||
CornerRadius="4"
|
||
Background="{DynamicResource Wd.Accent.Coral}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,6,0"/>
|
||
<TextBlock Style="{StaticResource Wd.Text.Mono}"
|
||
FontSize="11"
|
||
Foreground="{DynamicResource Wd.Accent.Coral}"
|
||
VerticalAlignment="Center">
|
||
<Run Text="REC "/>
|
||
<Run Text="{Binding ActiveRecordingCount, Mode=OneWay}"/>
|
||
<Run Text=" · "/>
|
||
<Run Text="{Binding RecordingElapsed, Mode=OneWay}"/>
|
||
</TextBlock>
|
||
</StackPanel>
|
||
|
||
<!-- Control surface badge — cyan dot + REST/OSC string when active -->
|
||
<StackPanel Grid.Column="4"
|
||
Orientation="Horizontal"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,16,0"
|
||
Visibility="{Binding IsControlSurfaceRunning, Converter={StaticResource BoolToVis}}"
|
||
ToolTip="External control surface is listening on localhost. See docs/CONTROL-SURFACE.md.">
|
||
<Border Width="8" Height="8"
|
||
CornerRadius="4"
|
||
Background="{DynamicResource Wd.Accent.Cyan}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,6,0"/>
|
||
<TextBlock Text="{Binding ControlSurfaceText}"
|
||
Style="{StaticResource Wd.Text.Mono}"
|
||
FontSize="11"
|
||
Foreground="{DynamicResource Wd.Accent.Cyan}"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
|
||
<TextBlock Grid.Column="5"
|
||
Text="wilddragon.net · 1.0.0-alpha"
|
||
Style="{StaticResource Wd.Text.Mono}"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
VerticalAlignment="Center"/>
|
||
</Grid>
|
||
</Border>
|
||
|
||
<!-- Body -->
|
||
<Grid Margin="32,28,32,28">
|
||
<Grid.RowDefinitions>
|
||
<RowDefinition Height="Auto"/>
|
||
<RowDefinition Height="Auto"/>
|
||
<RowDefinition Height="Auto"/>
|
||
<RowDefinition Height="Auto"/>
|
||
<RowDefinition Height="*"/>
|
||
</Grid.RowDefinitions>
|
||
|
||
<!--
|
||
Update-available banner. Hidden by default; shown by the launch-time
|
||
UpdateChecker if a newer release tag exists on Forgejo. "Get update"
|
||
opens the releases page; "Dismiss" hides until next launch.
|
||
-->
|
||
<Border Grid.Row="0"
|
||
Style="{StaticResource Wd.Card}"
|
||
Padding="14,10"
|
||
Margin="0,0,0,14"
|
||
Background="{DynamicResource Wd.Accent.CyanMuted}"
|
||
BorderBrush="{DynamicResource Wd.Accent.Cyan}"
|
||
Visibility="{Binding UpdateBanner.IsVisible, Converter={StaticResource BoolToVis}}">
|
||
<Grid>
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="*"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
</Grid.ColumnDefinitions>
|
||
<Border Grid.Column="0"
|
||
Width="8" Height="8"
|
||
CornerRadius="4"
|
||
Background="{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"
|
||
Foreground="{DynamicResource Wd.Text.Primary}"/>
|
||
<Button Grid.Column="2"
|
||
Style="{StaticResource Wd.Button.Ghost}"
|
||
Content="Get update"
|
||
Command="{Binding UpdateBanner.OpenReleasePageCommand}"
|
||
Padding="14,4"
|
||
Margin="0,0,8,0"
|
||
ToolTip="Open the Forgejo releases page in your browser to download the new MSI."/>
|
||
<Button Grid.Column="3"
|
||
Style="{StaticResource Wd.Button.Ghost}"
|
||
Content="Dismiss"
|
||
Command="{Binding UpdateBanner.DismissCommand}"
|
||
Padding="14,4"
|
||
ToolTip="Hide this banner. It'll reappear on next launch if the update is still available."/>
|
||
</Grid>
|
||
</Border>
|
||
|
||
<!--
|
||
Phase E.3 in-call control bar. Drives Microsoft Teams' UI via
|
||
UIAutomation so the operator can mute, toggle camera, share, or
|
||
leave without alt-tabbing to Teams (especially useful when the
|
||
Teams windows are hidden via the rail toggle). Buttons toast the
|
||
result; if Teams isn't in a call, the underlying buttons aren't
|
||
in the automation tree so the toast says so.
|
||
-->
|
||
<Border Grid.Row="1"
|
||
Style="{StaticResource Wd.Card}"
|
||
Padding="14,10"
|
||
Margin="0,0,0,18">
|
||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||
<TextBlock Text="IN-CALL"
|
||
Style="{StaticResource Wd.Text.Caption}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,12,0"/>
|
||
<!-- Teams meeting state pill — populated from a UIA probe
|
||
at the existing 1Hz stats tick. Visible only when Teams
|
||
is running. Cyan dot when in a call, gray when Teams is
|
||
open but idle. So an operator with Teams auto-hidden can
|
||
see at a glance whether they're in a meeting. -->
|
||
<Border Style="{StaticResource Wd.Pill}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,16,0"
|
||
Padding="10,3"
|
||
Visibility="{Binding HasTeamsState, Converter={StaticResource BoolToVis}}"
|
||
ToolTip="Microsoft Teams meeting state (probed via UIAutomation at 1Hz)">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Border Width="6" Height="6"
|
||
CornerRadius="3"
|
||
Margin="0,0,6,0"
|
||
VerticalAlignment="Center">
|
||
<Border.Style>
|
||
<Style TargetType="Border">
|
||
<Setter Property="Background" Value="{DynamicResource Wd.Text.Tertiary}"/>
|
||
<Style.Triggers>
|
||
<DataTrigger Binding="{Binding IsTeamsInCall}" Value="True">
|
||
<Setter Property="Background" Value="{DynamicResource Wd.Accent.Cyan}"/>
|
||
</DataTrigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
</Border.Style>
|
||
</Border>
|
||
<TextBlock Text="{Binding TeamsMeetingState}"
|
||
Style="{StaticResource Wd.Text.Caption}"
|
||
FontSize="10"
|
||
Foreground="{DynamicResource Wd.Text.Secondary}"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Border>
|
||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||
Command="{Binding ToggleMuteCommand}"
|
||
Padding="14,6"
|
||
Margin="0,0,8,0"
|
||
ToolTip="Toggle Microsoft Teams microphone mute">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Path Data="M 5,2 L 9,2 L 9,8 A 2,2 0 0 1 5,8 Z M 3,8 A 4,4 0 0 0 11,8 M 7,12 L 7,15 M 5,15 L 9,15"
|
||
Stroke="{DynamicResource Wd.Text.Primary}"
|
||
StrokeThickness="1.4"
|
||
Fill="Transparent"
|
||
Width="14" Height="16"
|
||
Stretch="None"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"/>
|
||
<TextBlock Text="Mute"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Button>
|
||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||
Command="{Binding ToggleCameraCommand}"
|
||
Padding="14,6"
|
||
Margin="0,0,8,0"
|
||
ToolTip="Toggle Microsoft Teams camera">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Path Data="M 1,3 L 11,3 L 11,11 L 1,11 Z M 11,5 L 15,3 L 15,11 L 11,9 Z"
|
||
Stroke="{DynamicResource Wd.Text.Primary}"
|
||
StrokeThickness="1.4"
|
||
Fill="Transparent"
|
||
Width="16" Height="14"
|
||
Stretch="None"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"/>
|
||
<TextBlock Text="Camera"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Button>
|
||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||
Command="{Binding OpenShareTrayCommand}"
|
||
Padding="14,6"
|
||
Margin="0,0,8,0"
|
||
ToolTip="Open the Microsoft Teams share tray">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Path Data="M 8,1 L 8,10 M 4,5 L 8,1 L 12,5 M 1,12 L 1,15 L 15,15 L 15,12"
|
||
Stroke="{DynamicResource Wd.Text.Primary}"
|
||
StrokeThickness="1.4"
|
||
Fill="Transparent"
|
||
Width="16" Height="16"
|
||
Stretch="None"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"/>
|
||
<TextBlock Text="Share"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Button>
|
||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||
Command="{Binding DropRecordingMarkerCommand}"
|
||
Padding="14,6"
|
||
Margin="0,0,8,0"
|
||
ToolTip="Drop a timestamped marker into every active recording. Useful for chaptering in post — 'guest answer starts here', 'highlight clip', etc. Markers land in each recording's manifest.json.">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Path Data="M 4,1 L 4,15 M 4,1 L 13,1 L 11,4 L 13,7 L 4,7"
|
||
Stroke="{DynamicResource Wd.Text.Primary}"
|
||
StrokeThickness="1.4"
|
||
Fill="Transparent"
|
||
Width="14" Height="16"
|
||
Stretch="None"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"/>
|
||
<TextBlock Text="Marker"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Button>
|
||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||
Command="{Binding ShowNotesCommand}"
|
||
Padding="14,6"
|
||
Margin="0,0,8,0"
|
||
ToolTip="Open the show-notes viewer for today. Notes can be appended via the REST or OSC notes endpoint or by typing in this dialog's editor link.">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Path Data="M 2,1 L 12,1 L 12,14 L 2,14 Z M 4,4 L 10,4 M 4,7 L 10,7 M 4,10 L 8,10"
|
||
Stroke="{DynamicResource Wd.Text.Primary}"
|
||
StrokeThickness="1.2"
|
||
Fill="Transparent"
|
||
Width="14" Height="15"
|
||
Stretch="None"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"/>
|
||
<TextBlock Text="Notes"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Button>
|
||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||
Command="{Binding LeaveCallCommand}"
|
||
Padding="14,6"
|
||
ToolTip="Leave the Microsoft Teams call">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Border Width="8" Height="8"
|
||
CornerRadius="4"
|
||
Background="{DynamicResource Wd.Accent.Coral}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"/>
|
||
<TextBlock Text="Leave"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Button>
|
||
|
||
<!-- Quick-join: paste a Teams meeting URL, click Join,
|
||
Teams launches into the meeting via the shell handler.
|
||
Avoids the open-Teams → Calendar → find meeting → click
|
||
join dance. Only enabled with non-empty text. -->
|
||
<Border Width="1"
|
||
Background="{DynamicResource Wd.Border}"
|
||
Margin="14,4,12,4"/>
|
||
<TextBox Text="{Binding JoinMeetingUrl, UpdateSourceTrigger=PropertyChanged}"
|
||
Width="240"
|
||
VerticalAlignment="Center"
|
||
Padding="10,5"
|
||
FontSize="11"
|
||
ToolTip="Paste a Teams meeting URL (https://teams.microsoft.com/l/meetup-join/... or msteams:/l/meetup-join/...) and click Join. TeamsISO hands it to Teams to launch + join in one step."/>
|
||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||
Command="{Binding JoinMeetingCommand}"
|
||
Padding="14,6"
|
||
Margin="6,0,0,0"
|
||
ToolTip="Hand the pasted meeting URL to Microsoft Teams. With auto-hide on (DISPLAY tab), the Teams window briefly appears then hides automatically.">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Path Data="M 8,1 L 8,11 M 4,7 L 8,11 L 12,7 M 1,14 L 15,14"
|
||
Stroke="{DynamicResource Wd.Accent.Cyan}"
|
||
StrokeThickness="1.5"
|
||
Fill="Transparent"
|
||
Width="16" Height="15"
|
||
Stretch="None"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"/>
|
||
<TextBlock Text="Join"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Button>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!-- Section header -->
|
||
<Grid Grid.Row="2">
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="*"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
</Grid.ColumnDefinitions>
|
||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||
<TextBlock Text="Participants"
|
||
Style="{StaticResource Wd.Text.Heading}"
|
||
FontSize="18"/>
|
||
<Border Style="{StaticResource Wd.Pill}"
|
||
Margin="12,0,0,0"
|
||
VerticalAlignment="Center">
|
||
<TextBlock Text="{Binding Participants.Count}"
|
||
Style="{StaticResource Wd.Text.Mono}"
|
||
FontSize="11"/>
|
||
</Border>
|
||
</StackPanel>
|
||
|
||
<!-- Header actions: Refresh discovery, Presets dialog, emergency stop. -->
|
||
<StackPanel Grid.Column="1"
|
||
Orientation="Horizontal"
|
||
VerticalAlignment="Center">
|
||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||
Command="{Binding EnableAllOnlineCommand}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"
|
||
ToolTip="Enable ISOs for every online participant who isn't already routing. Useful at show-start when everyone has just joined.">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Border Width="8" Height="8"
|
||
CornerRadius="4"
|
||
Background="{DynamicResource Wd.Status.Live}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"/>
|
||
<TextBlock Text="Enable all"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Button>
|
||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||
Command="{Binding RefreshDiscoveryCommand}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"
|
||
ToolTip="Force NDI discovery to rebuild — useful right after applying a new transcoder topology or after Teams restarts.">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Path Data="M 5,1 A 4,4 0 1 1 1.5,5 M 5,1 L 5,3 L 7,3"
|
||
Stroke="{DynamicResource Wd.Text.Primary}"
|
||
StrokeThickness="1.2"
|
||
Width="10" Height="10"
|
||
Stretch="None"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"/>
|
||
<TextBlock Text="Refresh"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Button>
|
||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||
Click="OnPresetsClick"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"
|
||
ToolTip="Save or load named ISO assignment snapshots for recurring shows">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Border Width="8" Height="8"
|
||
CornerRadius="4"
|
||
Background="{DynamicResource Wd.Accent.Cyan}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"/>
|
||
<TextBlock Text="Presets"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Button>
|
||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||
Command="{Binding StopAllIsosCommand}"
|
||
VerticalAlignment="Center"
|
||
ToolTip="Disable every running ISO immediately">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Border Width="8" Height="8"
|
||
CornerRadius="4"
|
||
Background="{DynamicResource Wd.Accent.Coral}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,8,0"/>
|
||
<TextBlock Text="Stop all ISOs"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</Button>
|
||
</StackPanel>
|
||
</Grid>
|
||
|
||
<Grid Grid.Row="3" Margin="0,6,0,18">
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="*"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
</Grid.ColumnDefinitions>
|
||
<TextBlock Grid.Column="0"
|
||
Text="Toggle a participant's ISO to spin up an isolated, normalized NDI output."
|
||
Style="{StaticResource Wd.Text.Subtle}"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
VerticalAlignment="Center"/>
|
||
<!-- Live filter input — case-insensitive substring match on display name -->
|
||
<TextBox Grid.Column="1"
|
||
Text="{Binding ParticipantFilter, UpdateSourceTrigger=PropertyChanged}"
|
||
MinWidth="200"
|
||
Padding="10,6"
|
||
FontSize="12"
|
||
VerticalAlignment="Center"
|
||
ToolTip="Filter the participants list as you type. Case-insensitive substring match against display name."/>
|
||
</Grid>
|
||
|
||
<!-- Participants card -->
|
||
<Border Grid.Row="4"
|
||
Style="{StaticResource Wd.Card}"
|
||
Padding="0">
|
||
<Grid>
|
||
<!-- Empty-state placeholder. Visible when Participants.Count == 0. -->
|
||
<StackPanel Visibility="{Binding Participants.Count, Converter={StaticResource CountToVis}, ConverterParameter=empty}"
|
||
HorizontalAlignment="Center"
|
||
VerticalAlignment="Center"
|
||
MaxWidth="460"
|
||
Margin="32,48">
|
||
<Image Source="/Assets/dragon-mark.png"
|
||
Width="64" Height="64"
|
||
Opacity="0.45"
|
||
HorizontalAlignment="Center"
|
||
Margin="0,0,0,20"
|
||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
||
<TextBlock Text="Waiting for Teams"
|
||
Style="{StaticResource Wd.Text.Heading}"
|
||
FontSize="16"
|
||
HorizontalAlignment="Center"/>
|
||
<TextBlock Style="{StaticResource Wd.Text.Subtle}"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
HorizontalAlignment="Center"
|
||
TextAlignment="Center"
|
||
TextWrapping="Wrap"
|
||
Margin="0,8,0,20"
|
||
Text="No Teams participants discovered on the network yet. Once Teams broadcasts NDI sources, this list will populate within a few seconds."/>
|
||
|
||
<Border Style="{StaticResource Wd.Card}"
|
||
Background="{DynamicResource Wd.SurfaceElevated}"
|
||
Padding="16,12">
|
||
<StackPanel>
|
||
<TextBlock Text="QUICK CHECKLIST"
|
||
Style="{StaticResource Wd.Text.Caption}"
|
||
Margin="0,0,0,8"/>
|
||
<TextBlock Style="{StaticResource Wd.Text.Subtle}"
|
||
Foreground="{DynamicResource Wd.Text.Secondary}"
|
||
TextWrapping="Wrap"
|
||
FontSize="12"
|
||
Margin="0,0,0,4">
|
||
<Run Text="•"/>
|
||
<Run Text=" Microsoft Teams is running and a meeting is active"/>
|
||
</TextBlock>
|
||
<TextBlock Style="{StaticResource Wd.Text.Subtle}"
|
||
Foreground="{DynamicResource Wd.Text.Secondary}"
|
||
TextWrapping="Wrap"
|
||
FontSize="12"
|
||
Margin="0,0,0,4">
|
||
<Run Text="•"/>
|
||
<Run Text=" NDI broadcast is enabled in Teams settings"/>
|
||
</TextBlock>
|
||
<TextBlock Style="{StaticResource Wd.Text.Subtle}"
|
||
Foreground="{DynamicResource Wd.Text.Secondary}"
|
||
TextWrapping="Wrap"
|
||
FontSize="12"
|
||
Margin="0,0,0,4">
|
||
<Run Text="•"/>
|
||
<Run Text=" Discovery group in Settings matches Teams' broadcast group"/>
|
||
</TextBlock>
|
||
<TextBlock Style="{StaticResource Wd.Text.Subtle}"
|
||
Foreground="{DynamicResource Wd.Text.Secondary}"
|
||
TextWrapping="Wrap"
|
||
FontSize="12">
|
||
<Run Text="•"/>
|
||
<Run Text=" Windows Firewall isn't blocking NDI multicast on the active network adapter"/>
|
||
</TextBlock>
|
||
</StackPanel>
|
||
</Border>
|
||
</StackPanel>
|
||
|
||
<!-- DataGrid: visible only when Participants.Count > 0. -->
|
||
<DataGrid ItemsSource="{Binding ParticipantsView}"
|
||
Visibility="{Binding Participants.Count, Converter={StaticResource CountToVis}}"
|
||
Margin="6,4,6,4">
|
||
<DataGrid.RowStyle>
|
||
<Style TargetType="DataGridRow">
|
||
<Setter Property="ContextMenu">
|
||
<Setter.Value>
|
||
<ContextMenu>
|
||
<MenuItem Header="Toggle ISO" Command="{Binding ToggleIsoCommand}"/>
|
||
<MenuItem Header="Restart this ISO"
|
||
Command="{Binding RestartIsoCommand}"
|
||
ToolTip="Disable + re-enable this pipeline only. Useful when a single feed flakes (drops climb, framerate jitters) without affecting other ISOs."/>
|
||
<MenuItem Header="Open preview…"
|
||
Command="{Binding OpenPreviewCommand}"
|
||
ToolTip="Pop out a floating live-preview window. Drag to a second monitor for full-screen monitoring."/>
|
||
<MenuItem Header="Save current frame…"
|
||
Command="{Binding SaveSnapshotCommand}"
|
||
ToolTip="Save the most recent processed frame as a PNG under %USERPROFILE%\Pictures\TeamsISO. Useful for highlight reels, social posts, or attaching to a bug report."/>
|
||
<MenuItem Header="Record this participant"
|
||
IsCheckable="True"
|
||
IsChecked="{Binding RecordToDisk}"/>
|
||
<Separator/>
|
||
<MenuItem Header="Copy NDI source name"
|
||
Command="{Binding CopySourceNameCommand}"
|
||
ToolTip="{Binding SourceFullName}"/>
|
||
</ContextMenu>
|
||
</Setter.Value>
|
||
</Setter>
|
||
</Style>
|
||
</DataGrid.RowStyle>
|
||
<DataGrid.Columns>
|
||
<!--
|
||
Live preview thumbnail. 160×90 (16:9) WriteableBitmap fed
|
||
from the engine's most recent ProcessedFrame at the 1Hz
|
||
stats tick. Falls back to the avatar initials when no
|
||
pipeline is running (Thumbnail is null).
|
||
-->
|
||
<DataGridTemplateColumn Header="Preview" Width="120">
|
||
<DataGridTemplateColumn.CellTemplate>
|
||
<DataTemplate>
|
||
<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
|
||
<!-- Placeholder shown when no thumbnail yet -->
|
||
<Border Width="100" Height="56"
|
||
Background="{DynamicResource Wd.SurfaceElevated}"
|
||
BorderBrush="{DynamicResource Wd.Border}"
|
||
BorderThickness="1"
|
||
CornerRadius="4"
|
||
Visibility="{Binding HasThumbnail, Converter={StaticResource InvertBool}}">
|
||
<TextBlock Text="—"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
FontFamily="{StaticResource Wd.Font.Mono}"
|
||
FontSize="11"
|
||
HorizontalAlignment="Center"
|
||
VerticalAlignment="Center"/>
|
||
</Border>
|
||
<!-- Live thumbnail. Stretch=Uniform preserves aspect; clip via Border for rounded corners -->
|
||
<Border Width="100" Height="56"
|
||
BorderBrush="{DynamicResource Wd.Border}"
|
||
BorderThickness="1"
|
||
CornerRadius="4"
|
||
ClipToBounds="True"
|
||
Visibility="{Binding HasThumbnail, Converter={StaticResource BoolToVis}}">
|
||
<Image Source="{Binding Thumbnail}"
|
||
Stretch="UniformToFill"
|
||
RenderOptions.BitmapScalingMode="LowQuality"/>
|
||
</Border>
|
||
</Grid>
|
||
</DataTemplate>
|
||
</DataGridTemplateColumn.CellTemplate>
|
||
</DataGridTemplateColumn>
|
||
|
||
<DataGridTemplateColumn Header="Display Name" Width="2*">
|
||
<DataGridTemplateColumn.CellTemplate>
|
||
<DataTemplate>
|
||
<StackPanel Orientation="Horizontal"
|
||
VerticalAlignment="Center"
|
||
ToolTip="{Binding SourceFullName}">
|
||
<Border Style="{StaticResource Wd.Avatar}">
|
||
<TextBlock Text="{Binding DisplayName, Converter={StaticResource Initials}}"
|
||
Foreground="{DynamicResource Wd.Accent.Cyan}"
|
||
FontFamily="{StaticResource Wd.Font.Sans}"
|
||
FontWeight="SemiBold"
|
||
FontSize="11"
|
||
HorizontalAlignment="Center"
|
||
VerticalAlignment="Center"/>
|
||
</Border>
|
||
<TextBlock Text="{Binding DisplayName}"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontWeight="Medium"
|
||
Margin="14,0,0,0"
|
||
VerticalAlignment="Center"/>
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</DataGridTemplateColumn.CellTemplate>
|
||
</DataGridTemplateColumn>
|
||
|
||
<DataGridTemplateColumn Header="Source" Width="*">
|
||
<DataGridTemplateColumn.CellTemplate>
|
||
<DataTemplate>
|
||
<StackPanel VerticalAlignment="Center">
|
||
<TextBlock Text="{Binding SourceMachine}"
|
||
Style="{StaticResource Wd.Text.Mono}"/>
|
||
<TextBlock Style="{StaticResource Wd.Text.Mono}"
|
||
FontSize="10"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}">
|
||
<Run Text="{Binding IncomingResolution, Mode=OneWay}"/>
|
||
<Run Text=" · "/>
|
||
<Run Text="{Binding IncomingFps, Mode=OneWay}"/>
|
||
</TextBlock>
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</DataGridTemplateColumn.CellTemplate>
|
||
</DataGridTemplateColumn>
|
||
|
||
<DataGridTemplateColumn Header="Live" Width="140">
|
||
<DataGridTemplateColumn.CellTemplate>
|
||
<DataTemplate>
|
||
<StackPanel VerticalAlignment="Center">
|
||
<TextBlock Style="{StaticResource Wd.Text.Mono}"
|
||
FontSize="11">
|
||
<Run Text="↓ "/>
|
||
<Run Text="{Binding FramesIn, Mode=OneWay}"/>
|
||
<Run Text=" ↑ "/>
|
||
<Run Text="{Binding FramesOut, Mode=OneWay}"/>
|
||
</TextBlock>
|
||
<TextBlock Style="{StaticResource Wd.Text.Mono}"
|
||
FontSize="10"
|
||
Foreground="{DynamicResource Wd.Accent.Coral}">
|
||
<Run Text="drop "/>
|
||
<Run Text="{Binding FramesDropped, Mode=OneWay}"/>
|
||
</TextBlock>
|
||
<!-- VU bar — width-bound to AudioLevelWidthPercent. The
|
||
outer Border is the 100-pixel-wide track; the inner
|
||
Border is the level fill. When the engine doesn't
|
||
yet feed PeakAudioLevel (current behavior), the bar
|
||
stays at 0 width. ToolTip explains the empty state. -->
|
||
<Border Width="100" Height="4"
|
||
HorizontalAlignment="Left"
|
||
Margin="0,4,0,0"
|
||
Background="{DynamicResource Wd.SurfaceElevated}"
|
||
CornerRadius="2"
|
||
ToolTip="Live audio peak. Engine audio capture is a follow-up; the bar will animate once that ships.">
|
||
<Border HorizontalAlignment="Left"
|
||
Width="{Binding AudioLevelWidthPercent, Mode=OneWay}"
|
||
Background="{DynamicResource Wd.Status.Live}"
|
||
CornerRadius="2"/>
|
||
</Border>
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</DataGridTemplateColumn.CellTemplate>
|
||
</DataGridTemplateColumn>
|
||
|
||
<DataGridTemplateColumn Header="Output Name" Width="2*">
|
||
<DataGridTemplateColumn.CellTemplate>
|
||
<DataTemplate>
|
||
<TextBox Text="{Binding CustomName, UpdateSourceTrigger=PropertyChanged}"
|
||
Padding="10,7"
|
||
Margin="0,0,12,0"
|
||
VerticalAlignment="Center"
|
||
ToolTip="Override the auto-generated NDI output name (TEAMSISO_xxxxxxxx). Takes effect when ISO is enabled."/>
|
||
</DataTemplate>
|
||
</DataGridTemplateColumn.CellTemplate>
|
||
</DataGridTemplateColumn>
|
||
|
||
<!--
|
||
Per-participant recording opt-out. When global recording is on,
|
||
only participants with this checkbox checked get a recorder when
|
||
their ISO starts. Read at EnableIsoAsync time so toggling on a
|
||
running pipeline has no effect — operator must disable + re-enable.
|
||
-->
|
||
<DataGridTemplateColumn Header="Rec" Width="60">
|
||
<DataGridTemplateColumn.CellTemplate>
|
||
<DataTemplate>
|
||
<CheckBox IsChecked="{Binding RecordToDisk}"
|
||
HorizontalAlignment="Center"
|
||
VerticalAlignment="Center"
|
||
ToolTip="When on, this participant's ISO is recorded to disk while global recording is enabled. Toggle while ISO is OFF — changes don't apply to a running pipeline."/>
|
||
</DataTemplate>
|
||
</DataGridTemplateColumn.CellTemplate>
|
||
</DataGridTemplateColumn>
|
||
|
||
<DataGridTemplateColumn Header="ISO" Width="130">
|
||
<DataGridTemplateColumn.CellTemplate>
|
||
<DataTemplate>
|
||
<Button Command="{Binding ToggleIsoCommand}"
|
||
Margin="0,0,12,0"
|
||
ToolTip="Toggle this participant's normalized NDI ISO output">
|
||
<Button.Style>
|
||
<Style TargetType="Button" BasedOn="{StaticResource Wd.Button.IsoToggle}">
|
||
<Setter Property="Content" Value="Enable"/>
|
||
<Setter Property="Foreground" Value="{DynamicResource Wd.Text.Secondary}"/>
|
||
<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.Cyan}"/>
|
||
</DataTrigger>
|
||
<!-- Pipeline failed past retry budget — surface the error state in coral. -->
|
||
<DataTrigger Binding="{Binding StateLabel}" Value="ERROR">
|
||
<Setter Property="Content" Value="● ERROR"/>
|
||
<Setter Property="Background" Value="{DynamicResource Wd.Accent.CoralBg}"/>
|
||
<Setter Property="BorderBrush" Value="{DynamicResource Wd.Accent.Coral}"/>
|
||
<Setter Property="Foreground" Value="{DynamicResource Wd.Accent.Coral}"/>
|
||
</DataTrigger>
|
||
<!-- No frames received in slate-threshold window — amber warning. -->
|
||
<DataTrigger Binding="{Binding StateLabel}" Value="NO SIGNAL">
|
||
<Setter Property="Content" Value="● NO SIGNAL"/>
|
||
<Setter Property="Foreground" Value="{DynamicResource Wd.Status.Warn}"/>
|
||
<Setter Property="BorderBrush" Value="{DynamicResource Wd.Status.Warn}"/>
|
||
</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>
|
||
|
||
<!-- Toast overlay: bottom-center, auto-dismissing transient notification.
|
||
Manual X dismiss for operators running a live show who want
|
||
to clear visual clutter without waiting the 3s auto-hide. -->
|
||
<Border Grid.Row="2"
|
||
VerticalAlignment="Bottom"
|
||
HorizontalAlignment="Center"
|
||
Margin="0,0,0,16"
|
||
Background="{DynamicResource Wd.SurfaceElevated}"
|
||
BorderBrush="{DynamicResource Wd.BorderStrong}"
|
||
BorderThickness="1"
|
||
CornerRadius="999"
|
||
Padding="14,6,8,6"
|
||
Visibility="{Binding Toast.IsVisible, Converter={StaticResource BoolToVis}}">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Ellipse Width="8" Height="8"
|
||
Fill="{DynamicResource Wd.Accent.Cyan}"
|
||
VerticalAlignment="Center"/>
|
||
<TextBlock Text="{Binding Toast.Message}"
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="12"
|
||
Margin="10,0,12,0"
|
||
VerticalAlignment="Center"/>
|
||
<Button Command="{Binding Toast.DismissCommand}"
|
||
Width="20" Height="20"
|
||
Background="Transparent"
|
||
BorderThickness="0"
|
||
Cursor="Hand"
|
||
FocusVisualStyle="{x:Null}"
|
||
ToolTip="Dismiss this toast">
|
||
<Button.Template>
|
||
<ControlTemplate TargetType="Button">
|
||
<Border x:Name="Bd"
|
||
Background="{TemplateBinding Background}"
|
||
CornerRadius="10">
|
||
<Path Data="M 0,0 L 8,8 M 8,0 L 0,8"
|
||
Stroke="{DynamicResource Wd.Text.Tertiary}"
|
||
StrokeThickness="1.2"
|
||
Width="8" Height="8"
|
||
Stretch="None"
|
||
HorizontalAlignment="Center"
|
||
VerticalAlignment="Center"/>
|
||
</Border>
|
||
<ControlTemplate.Triggers>
|
||
<Trigger Property="IsMouseOver" Value="True">
|
||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource Wd.Button.HoverBg}"/>
|
||
</Trigger>
|
||
</ControlTemplate.Triggers>
|
||
</ControlTemplate>
|
||
</Button.Template>
|
||
</Button>
|
||
</StackPanel>
|
||
</Border>
|
||
</Grid>
|
||
</DockPanel>
|
||
|
||
<!-- ════════════════════════════════════════════════════════════════
|
||
SETTINGS PANEL (right)
|
||
════════════════════════════════════════════════════════════════ -->
|
||
<Border Grid.Column="2"
|
||
Background="{DynamicResource Wd.Surface}"
|
||
BorderBrush="{DynamicResource Wd.Border}"
|
||
BorderThickness="1,0,0,0">
|
||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||
<StackPanel Margin="24,28,24,32">
|
||
|
||
<TextBlock Text="Settings"
|
||
Style="{StaticResource Wd.Text.Heading}"
|
||
FontSize="16"
|
||
Margin="0,0,0,4"/>
|
||
<TextBlock Text="Per-meeting global processing"
|
||
Style="{StaticResource Wd.Text.Subtle}"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
Margin="0,0,0,22"/>
|
||
|
||
<!--
|
||
Settings panel is grouped into three tabs (Output, Network, Display)
|
||
rather than one long scroll. Apply Changes lives outside the tabs so
|
||
it commits all three sections at once — the binding paths span groups
|
||
anyway (framerate + group strings + display toggles all roundtrip
|
||
through the same Settings.ApplyCommand).
|
||
-->
|
||
<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}"
|
||
ToolTip="The framerate every ISO output is normalized to. Click Apply to commit.">
|
||
<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}"
|
||
ToolTip="Output resolution. Source frames smaller than this are scaled up; larger frames are scaled down.">
|
||
<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}"
|
||
ToolTip="How to fit non-matching aspect ratios. Pillarbox = bars on the sides; Letterbox = bars top/bottom; Stretch = distort to fill.">
|
||
<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}"
|
||
ToolTip="Audio routing for ISO outputs. Auto = isolated when available, fall back to mixed; Isolated = participant audio only; Mixed = full meeting mix.">
|
||
<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"
|
||
ToolTip="Restore framerate, resolution, aspect and audio to TeamsISO defaults. Doesn't touch NDI groups or display toggles."/>
|
||
</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}"
|
||
ToolTip="Comma-separated NDI group names the engine subscribes to. Empty = Public (default)."/>
|
||
<TextBlock Text="Receive sources from this group. Empty = Public."
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="11"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
Margin="0,4,0,0"/>
|
||
|
||
<TextBlock Text="Output group"
|
||
Style="{StaticResource Wd.Text.Subtle}"
|
||
Margin="0,12,0,4"/>
|
||
<TextBox Text="{Binding Settings.OutputGroups, UpdateSourceTrigger=PropertyChanged}"
|
||
ToolTip="Comma-separated NDI group(s) TeamsISO's normalized ISO outputs broadcast on. Empty = Public."/>
|
||
<TextBlock Text="Broadcast TeamsISO outputs on this group. Empty = Public."
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="11"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
Margin="0,4,0,0"/>
|
||
|
||
<Border Background="{DynamicResource Wd.SurfaceElevated}"
|
||
BorderBrush="{DynamicResource Wd.Border}"
|
||
BorderThickness="1"
|
||
CornerRadius="{StaticResource Radius.M}"
|
||
Padding="12,10"
|
||
Margin="0,12,0,0">
|
||
<TextBlock Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="11"
|
||
Foreground="{DynamicResource Wd.Text.Secondary}"
|
||
TextWrapping="Wrap"
|
||
Text="Group changes apply on next launch — running pipelines aren't restarted to avoid orphaning live ISOs."/>
|
||
</Border>
|
||
|
||
<!-- One-click transcoder topology setup. Writes the system-wide
|
||
NDI config so Teams broadcasts on a private group, then sets
|
||
the engine to consume from that group and re-emit on Public. -->
|
||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||
Content="Apply transcoder topology"
|
||
Command="{Binding Settings.ApplyTranscoderTopologyCommand}"
|
||
HorizontalAlignment="Stretch"
|
||
Margin="0,12,0,0"
|
||
Padding="0,9"/>
|
||
<TextBlock Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="11"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
TextWrapping="Wrap"
|
||
Margin="0,4,0,0"
|
||
Text="Pins Teams' raw NDI broadcasts to a private 'teamsiso-input' group so they don't pollute the Public network. Restart Teams after applying."/>
|
||
|
||
<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"
|
||
ToolTip="Template applied to NDI output names when no per-participant Custom Name is set. Tokens: {name} = display name, {guid} = first-8 of Id, {machine} = PC name, {timestamp} = yyyyMMdd_HHmmss."/>
|
||
<TextBlock Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="11"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
TextWrapping="Wrap"
|
||
Margin="0,4,0,0"
|
||
Text="Tokens: {name}, {guid}, {machine}, {timestamp}. Default: TEAMSISO_{guid}. Use TEAMSISO_{name} for human-readable downstream switcher names."/>
|
||
</StackPanel>
|
||
</TabItem>
|
||
|
||
<!-- ──────────────── Display ──────────────── -->
|
||
<TabItem Header="DISPLAY" Style="{StaticResource Wd.TabItem}">
|
||
<StackPanel>
|
||
<CheckBox Content="Hide my self-preview from participants"
|
||
IsChecked="{Binding Settings.HideLocalSelf}"
|
||
ToolTip="Filters out the '(Local)' source — your own preview that Teams broadcasts on the same machine — so you don't accidentally route it as an ISO."/>
|
||
|
||
<CheckBox Content="Auto-disable ISOs when a participant leaves"
|
||
IsChecked="{Binding Settings.AutoDisableOnDeparture}"
|
||
ToolTip="When checked, departing participants automatically have their ISO pipelines torn down. Off by default so transient drops don't lose your routing."
|
||
Margin="0,10,0,0"/>
|
||
|
||
<CheckBox Content="Auto-apply last preset on launch"
|
||
IsChecked="{Binding Settings.AutoApplyLastPreset}"
|
||
ToolTip="On launch, automatically re-apply the most recently applied operator preset once participants populate. Useful for recurring shows where the same routing should be restored every session."
|
||
Margin="0,10,0,0"/>
|
||
|
||
<TextBlock Text="Sort participants by"
|
||
Style="{StaticResource Wd.Text.Subtle}"
|
||
Margin="0,12,0,4"/>
|
||
<ComboBox ItemsSource="{Binding Settings.AvailableSortModes}"
|
||
SelectedItem="{Binding Settings.ParticipantSort}"
|
||
ToolTip="JoinOrder = whatever order Teams emits sources (default). Alphabetical = stable across meetings, good for Stream Deck button assignments. OnlineFirst = online participants float to the top."/>
|
||
|
||
<CheckBox Content="Minimize to system tray"
|
||
IsChecked="{Binding Settings.MinimizeToTray}"
|
||
Margin="0,12,0,0"
|
||
ToolTip="When checked, minimizing the window hides it and shows a tray icon. Useful for long unattended shows. Double-click the tray icon to restore."/>
|
||
|
||
<!-- Phase E.1/E.2 "I only see TeamsISO" pair. With both ticked,
|
||
the operator launches TeamsISO and never sees the Teams UI —
|
||
Teams runs in the background and all interaction routes
|
||
through the IN-CALL bar + participants DataGrid. -->
|
||
<CheckBox Content="Launch Microsoft Teams on TeamsISO startup"
|
||
IsChecked="{Binding Settings.LaunchTeamsOnStartup}"
|
||
Margin="0,16,0,0"
|
||
ToolTip="When checked, TeamsISO will auto-launch Microsoft Teams in the background each time it starts. Combine with 'Auto-hide Teams windows' for the 'I only see TeamsISO' experience."/>
|
||
|
||
<CheckBox Content="Auto-hide Teams windows when launched"
|
||
IsChecked="{Binding Settings.AutoHideTeamsWindows}"
|
||
Margin="0,8,0,0"
|
||
ToolTip="When checked, Teams' windows are hidden as soon as they materialize after launch. Use the eye-icon button in the rail to restore them when needed. Drives Teams via the IN-CALL bar (mute / camera / share / leave / marker) and the participants DataGrid for ISO routing."/>
|
||
|
||
<CheckBox Content="Auto-record when Teams joins a meeting"
|
||
IsChecked="{Binding Settings.AutoRecordOnCall}"
|
||
Margin="0,8,0,0"
|
||
ToolTip="When checked, recording auto-starts the moment Teams transitions into a call (IN CALL pill goes cyan) and auto-stops when the call ends. Removes the manual Record toggle step from unattended-show workflows."/>
|
||
|
||
<!-- Phase E.4 experimental — SetParent embed. WebView2 in modern
|
||
Teams can render weirdly after reparent; if so, untick and
|
||
fall back to auto-hide mode. -->
|
||
<Border Margin="0,12,0,8"
|
||
Height="1"
|
||
Background="{DynamicResource Wd.Border}"/>
|
||
<CheckBox Content="Embed Teams window inside TeamsISO (experimental)"
|
||
IsChecked="{Binding Settings.EmbedTeamsWindow}"
|
||
Margin="0,4,0,0"
|
||
ToolTip="EXPERIMENTAL: Reparent Teams' main window into a TeamsISO-owned host so Teams appears INSIDE our window. WebView2 in modern Teams may render glitches or refuse focus after reparent — if so, untick and use auto-hide mode instead. Click 'Open embed window' below after enabling."/>
|
||
<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}"
|
||
ToolTip="Open the embed host window. Teams' main window will be reparented into it on load. Close the window to restore Teams to normal top-level state."/>
|
||
|
||
<Separator Margin="0,16,0,8"/>
|
||
|
||
<CheckBox Content="Record ISOs to disk"
|
||
IsChecked="{Binding Settings.RecordIsosToDisk}"
|
||
ToolTip="When checked, each newly-enabled ISO writes raw BGRA frames + manifest.json + convert.cmd to its own subdirectory. Run convert.cmd to produce H.264 .mkv via FFmpeg. Recording starts on the next ISO enable; already-running ISOs aren't retroactively captured."/>
|
||
<TextBlock Text="Output directory"
|
||
Style="{StaticResource Wd.Text.Subtle}"
|
||
Margin="0,12,0,4"
|
||
IsEnabled="{Binding Settings.RecordIsosToDisk}"/>
|
||
<TextBox Text="{Binding Settings.RecordingDirectory, UpdateSourceTrigger=PropertyChanged}"
|
||
IsEnabled="{Binding Settings.RecordIsosToDisk}"
|
||
ToolTip="Root directory for recordings. Each participant gets a subdirectory under this path. Default: %USERPROFILE%\Videos\TeamsISO\<date>."/>
|
||
<TextBlock Text="Recordings live under this path. Each ISO writes raw BGRA + manifest.json. Double-click the convert.cmd inside to produce a final H.264 .mkv."
|
||
Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="11"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
TextWrapping="Wrap"
|
||
Margin="0,4,0,0"/>
|
||
|
||
<Separator Margin="0,16,0,8"/>
|
||
|
||
<CheckBox Content="Control surface (Stream Deck / Companion / web)"
|
||
IsChecked="{Binding Settings.ControlSurfaceEnabled}"
|
||
ToolTip="Start an HTTP server so external controllers (Bitfocus Companion, Stream Deck plugins, Bome MIDI Translator) or a thin-client browser can drive TeamsISO."/>
|
||
|
||
<CheckBox Content="LAN-reachable (allow other machines on your network)"
|
||
IsChecked="{Binding Settings.ControlSurfaceLanReachable}"
|
||
Margin="20,4,0,0"
|
||
ToolTip="When checked, the control surface binds to all interfaces (0.0.0.0) instead of 127.0.0.1, so a phone or laptop on your LAN can drive TeamsISO. First time you turn this on, run this in an Administrator PowerShell once: netsh http add urlacl url=http://+:9755/ user=Everyone"/>
|
||
|
||
<Grid Margin="20,4,0,0">
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="*"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
</Grid.ColumnDefinitions>
|
||
<TextBlock Grid.Column="0"
|
||
Text="{Binding Settings.ControlSurfaceUrl}"
|
||
Style="{StaticResource Wd.Text.Mono}"
|
||
FontSize="11"
|
||
Foreground="{DynamicResource Wd.Accent.Cyan}"
|
||
VerticalAlignment="Center"
|
||
TextTrimming="CharacterEllipsis"/>
|
||
<Button Grid.Column="1"
|
||
Style="{StaticResource Wd.Button.Ghost}"
|
||
Content="Open"
|
||
Command="{Binding Settings.OpenControlSurfaceCommand}"
|
||
Padding="10,2"
|
||
Margin="0,0,4,0"
|
||
FontSize="11"
|
||
ToolTip="Open this URL in your default browser. Useful for previewing how the control panel looks before pointing a phone or second-monitor at it."/>
|
||
<Button Grid.Column="2"
|
||
Style="{StaticResource Wd.Button.Ghost}"
|
||
Content="Copy"
|
||
Command="{Binding Settings.CopyControlSurfaceUrlCommand}"
|
||
Padding="10,2"
|
||
FontSize="11"
|
||
ToolTip="Copy this URL to the clipboard so you can paste it into a phone browser or controller."/>
|
||
</Grid>
|
||
<Grid Margin="0,8,0,0">
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="*"/>
|
||
</Grid.ColumnDefinitions>
|
||
<TextBlock Grid.Column="0"
|
||
Text="Port"
|
||
Style="{StaticResource Wd.Text.Subtle}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,12,0"/>
|
||
<TextBox Grid.Column="1"
|
||
Text="{Binding Settings.ControlSurfacePort, UpdateSourceTrigger=LostFocus}"
|
||
ToolTip="Listening port for the REST control surface. Default 9755."/>
|
||
</Grid>
|
||
<TextBlock Style="{StaticResource Wd.Text.Body}"
|
||
FontSize="11"
|
||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
||
TextWrapping="Wrap"
|
||
Margin="0,4,0,0"
|
||
Text="Endpoints: GET /participants, POST /participants/{id}/iso, /presets/{name}/apply, /presets/refresh-discovery, /presets/stop-all, /teams/mute|camera|leave|share|raise-hand, /recording. WebSocket /ws pushes live state. See docs/CONTROL-SURFACE.md."/>
|
||
|
||
<CheckBox Content="OSC bridge (UDP)"
|
||
IsChecked="{Binding Settings.OscBridgeEnabled}"
|
||
Margin="0,12,0,0"
|
||
ToolTip="UDP listener that speaks the same command surface as REST. /teamsiso/iso 'Jane' 1, /teamsiso/preset 'Friday Show', /teamsiso/teams/mute, etc. Bound to 127.0.0.1 only."/>
|
||
<Grid Margin="0,8,0,0">
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="*"/>
|
||
</Grid.ColumnDefinitions>
|
||
<TextBlock Grid.Column="0"
|
||
Text="OSC port"
|
||
Style="{StaticResource Wd.Text.Subtle}"
|
||
VerticalAlignment="Center"
|
||
Margin="0,0,12,0"/>
|
||
<TextBox Grid.Column="1"
|
||
Text="{Binding Settings.OscBridgePort, UpdateSourceTrigger=LostFocus}"
|
||
ToolTip="UDP port for the OSC bridge. Default 9000 (TouchOSC's default)."/>
|
||
</Grid>
|
||
|
||
<Separator Margin="0,16,0,8"/>
|
||
|
||
<CheckBox Content="Check for updates on launch"
|
||
IsChecked="{Binding Settings.UpdateCheckOnLaunch}"
|
||
ToolTip="On startup, ask forge.wilddragon.net whether a newer release exists. Throttled to once per 24h. If newer, a banner appears at the top of the participants area; click 'Get update' to open the releases page in your browser."/>
|
||
</StackPanel>
|
||
</TabItem>
|
||
</TabControl>
|
||
|
||
<!--
|
||
Apply Changes commits OUTPUT + NETWORK fields together. DISPLAY-tab
|
||
toggles persist on each click (HideLocalSelf is in-memory only;
|
||
AutoDisableOnDeparture / AutoApplyLastPreset write through to disk
|
||
from their setters), so they don't strictly need this button — but
|
||
keeping it visible from any tab gives the user a single confirm.
|
||
-->
|
||
<Button Style="{StaticResource Wd.Button.Primary}"
|
||
Content="Apply Changes"
|
||
Command="{Binding Settings.ApplyCommand}"
|
||
HorizontalAlignment="Stretch"
|
||
Margin="0,28,0,0"
|
||
Padding="0,11"
|
||
ToolTip="Persist all settings to %APPDATA%\TeamsISO\config.json. Group changes apply on next launch."/>
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
</Border>
|
||
|
||
</Grid>
|
||
</Window>
|