Lumi/companion/src/Lumi.Companion.App/MainWindow.axaml
2026-07-26 22:06:34 +02:00

593 lines
42 KiB
XML

<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Lumi.Companion.App.MainWindow"
Title="Lumi Companion"
Icon="/Assets/Lumi.Companion.ico"
Width="1120" Height="760" MinWidth="900" MinHeight="620"
WindowStartupLocation="CenterScreen">
<Grid ColumnDefinitions="248,*">
<Border Grid.Column="0" Background="#F4F7F8" Padding="22,24">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<StackPanel Orientation="Horizontal" Spacing="11">
<Border Width="34" Height="34" CornerRadius="9" Background="#28B7C8">
<Path Data="M 17,7 L 27,17 L 17,27 L 7,17 Z" Fill="White" Stretch="None" />
</Border>
<StackPanel VerticalAlignment="Center" Spacing="0">
<TextBlock Text="LUMI" Classes="eyebrow" />
<TextBlock Text="Companion" FontWeight="SemiBold" FontSize="16" />
</StackPanel>
</StackPanel>
<Border Grid.Row="1" Margin="0,25,0,20" Padding="11,9" CornerRadius="9" Background="#E6EEF0">
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="9">
<Border x:Name="StatusMark" Width="10" Height="10" CornerRadius="5" Background="#A96612" VerticalAlignment="Center" />
<StackPanel Grid.Column="1" Spacing="1">
<TextBlock x:Name="StatusLabel" Text="Setup required" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="StatusSymbol" Text="○ Incomplete" FontSize="11" Foreground="{DynamicResource LumiMutedBrush}" />
</StackPanel>
</Grid>
</Border>
<Grid Grid.Row="2" RowDefinitions="Auto,*,Auto">
<StackPanel Spacing="4">
<Button x:Name="OverviewNav" Classes="nav selected" Content="Overview" Tag="Overview" />
<Button x:Name="StreamTestingNav" Classes="nav" Content="Stream testing" Tag="StreamTesting" />
</StackPanel>
<ScrollViewer Grid.Row="1" Margin="0,14,0,14" VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="6">
<TextBlock Text="PLUGINS" Classes="eyebrow" Margin="12,0,0,4" />
<StackPanel x:Name="PluginNavigation" Spacing="4" />
</StackPanel>
</ScrollViewer>
<StackPanel Grid.Row="2" x:Name="TechnicalNavigation">
<Border Height="1" Background="{DynamicResource LumiBorderBrush}" Margin="0,0,0,8" />
<Button x:Name="ConnectionNav" Classes="nav" Content="Connection &amp; device" Tag="Connection" />
<Button x:Name="LogsNav" Classes="nav" Content="Logs &amp; diagnostics" Tag="Logs" />
<Button x:Name="SettingsNav" Classes="nav" Content="Settings" Tag="Settings" />
</StackPanel>
</Grid>
<TextBlock Grid.Row="3" Text="Runs quietly in the notification area" Classes="muted" FontSize="11" />
</Grid>
</Border>
<Grid Grid.Column="1">
<ScrollViewer x:Name="OverviewPage">
<StackPanel Margin="44,38,52,48" MaxWidth="760" HorizontalAlignment="Left" Spacing="24">
<StackPanel Spacing="6">
<TextBlock Text="AT A GLANCE" Classes="eyebrow" />
<TextBlock Text="Overview" Classes="pageTitle" />
<TextBlock x:Name="OverviewDetail" Text="Checking what is ready…" Classes="muted" FontSize="15" />
</StackPanel>
<Border Classes="soft" Padding="22">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="24">
<StackPanel Spacing="6">
<TextBlock x:Name="NextActionTitle" Text="Pair this computer" Classes="sectionTitle" />
<TextBlock x:Name="NextActionDetail" Text="Use a one-time pairing package from Lumi to connect this streaming computer." Classes="muted" MaxWidth="520" />
</StackPanel>
<Button Grid.Column="1" x:Name="NextActionButton" Classes="primary" Content="Choose pairing package" VerticalAlignment="Center" />
</Grid>
</Border>
<Border x:Name="UpdatePanel" Classes="soft" Padding="18" IsVisible="False">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="20">
<StackPanel Spacing="4">
<TextBlock x:Name="UpdateTitle" Text="Companion update available" FontWeight="SemiBold" />
<TextBlock x:Name="UpdateDetail" Text="A verified update is ready." Classes="muted" />
</StackPanel>
<Button Grid.Column="1" x:Name="ApplyUpdateButton" Classes="primary" Content="Update Companion" VerticalAlignment="Center" />
</Grid>
</Border>
<Border x:Name="OverviewBridgeUpdatePanel" Background="#FFF2D6" BorderBrush="#D58A1F" BorderThickness="1" CornerRadius="12" Padding="18" IsVisible="False">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="20">
<StackPanel Spacing="4">
<TextBlock Text="Update OBS Bridge" FontWeight="Bold" Foreground="#764600" />
<TextBlock x:Name="OverviewBridgeUpdateDetail" Text="The OBS integration loaded by OBS is older than the version bundled with Companion." TextWrapping="Wrap" Foreground="#764600" />
</StackPanel>
<Button Grid.Column="1" x:Name="OverviewUpdateBridgeButton" Classes="primary" Content="Update OBS Bridge" VerticalAlignment="Center" />
</Grid>
</Border>
<StackPanel Spacing="12">
<TextBlock Text="Setup progress" Classes="sectionTitle" />
<Grid ColumnDefinitions="*,*,*" ColumnSpacing="12">
<Border Classes="card">
<StackPanel Spacing="7">
<TextBlock x:Name="PairingStepSymbol" Text="○" FontSize="20" />
<TextBlock Text="Lumi connection" FontWeight="SemiBold" />
<TextBlock x:Name="PairingStepDetail" Text="Not paired" Classes="muted" FontSize="12" />
</StackPanel>
</Border>
<Border Grid.Column="1" Classes="card">
<StackPanel Spacing="7">
<TextBlock x:Name="ObsStepSymbol" Text="○" FontSize="20" />
<TextBlock Text="OBS integration" FontWeight="SemiBold" />
<TextBlock x:Name="ObsStepDetail" Text="Not installed" Classes="muted" FontSize="12" />
</StackPanel>
</Border>
<Border Grid.Column="2" Classes="card">
<StackPanel Spacing="7">
<TextBlock x:Name="TestStepSymbol" Text="○" FontSize="20" />
<TextBlock Text="Full path test" FontWeight="SemiBold" />
<TextBlock x:Name="TestStepDetail" Text="Not run" Classes="muted" FontSize="12" />
</StackPanel>
</Border>
</Grid>
</StackPanel>
<Border Classes="soft">
<TextBlock Text="Lumi Companion never runs speech recognition on this streaming computer. Audio stays in bounded memory and is sent to your paired Lumi host." Classes="muted" />
</Border>
</StackPanel>
</ScrollViewer>
<ScrollViewer x:Name="StreamTestingPage" IsVisible="False">
<StackPanel Margin="44,38,52,48" MaxWidth="760" HorizontalAlignment="Left" Spacing="24">
<StackPanel Spacing="6">
<TextBlock Text="PRIVATE OUTPUT" Classes="eyebrow" />
<TextBlock Text="Stream testing" Classes="pageTitle" />
<TextBlock Text="Send the real OBS stream to Lumi's private test receiver, inspect it in the Admin page, then restore the exact normal streaming service automatically." Classes="muted" FontSize="15" />
</StackPanel>
<Border Background="#FFF2D6" BorderBrush="#D58A1F" BorderThickness="1" CornerRadius="12" Padding="18">
<StackPanel Spacing="5">
<TextBlock Text="Never start this while publicly live" FontWeight="Bold" Foreground="#764600" />
<TextBlock Text="Companion refuses to redirect active streaming or recording. While the test is active, the status below remains unmistakable and the prior OBS service is kept in an encrypted current-user recovery file." TextWrapping="Wrap" Foreground="#764600" />
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="16">
<StackPanel Spacing="5">
<TextBlock x:Name="StreamTestStatusTitle" Text="Ready to check" Classes="sectionTitle" />
<TextBlock x:Name="StreamTestStatusDetail" Text="Start only while OBS is idle." Classes="muted" TextWrapping="Wrap" />
</StackPanel>
<Grid ColumnDefinitions="*,*,*" ColumnSpacing="12">
<Border Classes="soft"><StackPanel Spacing="4"><TextBlock Text="OBS bitrate" Classes="muted" FontSize="11" /><TextBlock x:Name="StreamTestBitrateText" Text="—" FontWeight="SemiBold" /></StackPanel></Border>
<Border Grid.Column="1" Classes="soft"><StackPanel Spacing="4"><TextBlock Text="Dropped frames" Classes="muted" FontSize="11" /><TextBlock x:Name="StreamTestDroppedText" Text="—" FontWeight="SemiBold" /></StackPanel></Border>
<Border Grid.Column="2" Classes="soft"><StackPanel Spacing="4"><TextBlock Text="Congestion" Classes="muted" FontSize="11" /><TextBlock x:Name="StreamTestCongestionText" Text="—" FontWeight="SemiBold" /></StackPanel></Border>
</Grid>
<StackPanel Orientation="Horizontal" Spacing="10">
<Button x:Name="StartStreamTestButton" Classes="primary" Content="Start private stream test" />
<Button x:Name="StopStreamTestButton" Classes="secondary" Content="End test and restore OBS" IsEnabled="False" />
<Button x:Name="OpenStreamTestWebButton" Classes="secondary" Content="Open stream viewer" IsVisible="False" />
<Button x:Name="RepairStreamTestButton" Classes="secondary" Content="Repair restoration" IsVisible="False" />
</StackPanel>
</StackPanel>
</Border>
<Border Classes="soft">
<StackPanel Spacing="5">
<TextBlock Text="Where to watch" FontWeight="SemiBold" />
<TextBlock Text="Open Admin → Stream testing in Lumi WebUI for the private player, quality control, receiver facts, warnings, and recovery timeline." Classes="muted" TextWrapping="Wrap" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<ScrollViewer x:Name="TranscriptionPage" IsVisible="False">
<StackPanel Margin="44,38,52,48" MaxWidth="760" HorizontalAlignment="Left" Spacing="25">
<StackPanel Spacing="6">
<TextBlock Text="CAPTURE" Classes="eyebrow" />
<TextBlock Text="Transcription" Classes="pageTitle" />
<TextBlock Text="Choose the OBS microphone Lumi should caption. Source identity remains stable if you rename it." Classes="muted" FontSize="15" />
</StackPanel>
<Border Classes="card">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="24">
<StackPanel Spacing="4">
<TextBlock Text="Generate and include captions" FontWeight="SemiBold" />
<TextBlock x:Name="TranscriptionEnabledDetail" Text="Speech recognition and caption inclusion are enabled." Classes="muted" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="TranscriptionEnabledToggle" OffContent="Off" OnContent="On" VerticalAlignment="Center" />
</Grid>
</Border>
<StackPanel Spacing="8">
<TextBlock Text="Primary microphone" FontWeight="SemiBold" />
<ComboBox x:Name="SourcePicker" IsEnabled="False" PlaceholderText="Open OBS after installing the managed integration" MinWidth="420" HorizontalAlignment="Left" />
<TextBlock x:Name="SourceHelp" Text="No OBS source list is available yet." Classes="muted" />
</StackPanel>
<Border Classes="soft">
<StackPanel Spacing="7">
<TextBlock Text="Optional second microphone" FontWeight="SemiBold" />
<TextBlock Text="The protocol supports more than one source, but source overlap policy remains disabled until the primary path is validated." Classes="muted" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<ScrollViewer x:Name="TestPage" IsVisible="False">
<StackPanel Margin="44,38,52,48" MaxWidth="760" HorizontalAlignment="Left" Spacing="24">
<StackPanel Spacing="6">
<TextBlock Text="SAFE CHECK" Classes="eyebrow" />
<TextBlock Text="Transcription test" Classes="pageTitle" />
<TextBlock Text="Checks the selected OBS source, secure connection, model readiness, and safe caption return path. You do not need to speak, and nothing is sent to Twitch." Classes="muted" FontSize="15" />
</StackPanel>
<Button x:Name="RunTestButton" Classes="primary" Content="Run full path test" HorizontalAlignment="Left" />
<StackPanel x:Name="TestStagesPanel" Spacing="8" />
<Border Classes="soft">
<StackPanel Spacing="5">
<TextBlock Text="Simulated outgoing caption" FontWeight="SemiBold" />
<TextBlock x:Name="SimulatedCaption" Text="A real caption will appear here only after every upstream stage passes." Classes="muted" FontStyle="Italic" />
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="16">
<StackPanel Spacing="5">
<TextBlock Text="ACCURACY &amp; LATENCY" Classes="eyebrow" />
<TextBlock Text="Dedicated transcription test" Classes="sectionTitle" />
<TextBlock Text="Speak naturally for as long as needed. End the test yourself, or Lumi will finalize it after 10 seconds of silence. Nothing is sent to Twitch." Classes="muted" />
</StackPanel>
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="10">
<Button x:Name="StartBenchmarkButton" Classes="primary" Content="Start dedicated test" />
<Button Grid.Column="1" x:Name="StopBenchmarkButton" Classes="secondary" Content="End test" IsEnabled="False" />
<ProgressBar Grid.Column="2" x:Name="VoiceLevelMeter" Minimum="-60" Maximum="0" Value="-60" Height="12" MinWidth="220" VerticalAlignment="Center" Foreground="{DynamicResource LumiDangerBrush}" />
<TextBlock Grid.Column="3" x:Name="VoiceLevelText" Text="-60 dBFS" Classes="muted" FontSize="11" VerticalAlignment="Center" MinWidth="58" />
</Grid>
<TextBlock Text="Input level: green from -30 to -12 dBFS; orange within 10 dB outside that range; red otherwise." Classes="muted" FontSize="11" />
<TextBlock x:Name="BenchmarkStatusText" Text="Not started." Classes="muted" />
<Border Classes="soft">
<StackPanel Spacing="5">
<TextBlock Text="Measured transcript" FontWeight="SemiBold" />
<TextBlock x:Name="BenchmarkTranscript" Text="Finalized speech will appear here." Classes="muted" TextWrapping="Wrap" />
</StackPanel>
</Border>
<StackPanel Spacing="8">
<TextBlock Text="Latency" FontWeight="SemiBold" />
<WrapPanel x:Name="LatencyStatsPanel" Orientation="Horizontal" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="Final confidence" FontWeight="SemiBold" />
<WrapPanel x:Name="ConfidenceStatsPanel" Orientation="Horizontal" />
</StackPanel>
<TextBlock Text="Low and high 1% values are tail averages. Confidence is included only after an utterance is finalized." Classes="muted" FontSize="12" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<ScrollViewer x:Name="SongOverlayPage" IsVisible="False">
<StackPanel Margin="44,38,52,48" MaxWidth="760" HorizontalAlignment="Left" Spacing="24">
<StackPanel Spacing="6">
<TextBlock Text="MEDIA" Classes="eyebrow" />
<TextBlock Text="Song Overlay" Classes="pageTitle" />
<TextBlock Text="Spotify is the first media provider. The provider contract remains generic so later services can use the same event and overlay pipeline." Classes="muted" FontSize="15" />
</StackPanel>
<Border Classes="soft">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="20">
<StackPanel Spacing="4">
<TextBlock x:Name="SongOverlayStatusTitle" Text="Starting" FontWeight="SemiBold" />
<TextBlock x:Name="SongOverlayStatusDetail" Text="Waiting for the media provider…" Classes="muted" />
<TextBlock x:Name="SongOverlayCurrentTrack" Text="No active song" Classes="muted" FontSize="12" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="SongOverlayEnabledToggle" OffContent="Off" OnContent="On" VerticalAlignment="Center" />
</Grid>
</Border>
<StackPanel Spacing="10">
<TextBlock Text="Lumi authentication" Classes="sectionTitle" />
<Border Classes="soft">
<StackPanel Spacing="4">
<TextBlock x:Name="SongOverlayLumiAuthenticationText" Text="Uses the Companion device pairing" FontWeight="SemiBold" />
<TextBlock Text="Song Overlay inherits the protected Companion credential automatically. Revoking or forgetting this Companion also revokes plugin access; no separate plugin key is needed." Classes="muted" />
</StackPanel>
</Border>
</StackPanel>
<StackPanel Spacing="12">
<TextBlock Text="Playback provider" Classes="sectionTitle" />
<Grid ColumnDefinitions="150,*" RowDefinitions="Auto,Auto,Auto" RowSpacing="10" ColumnSpacing="12">
<TextBlock Text="Provider" Classes="muted" VerticalAlignment="Center" />
<ComboBox Grid.Column="1" x:Name="SongOverlayProviderPicker" SelectedIndex="0" IsEnabled="False">
<ComboBoxItem Content="Spotify" Tag="spotify" />
</ComboBox>
<TextBlock Grid.Row="1" Text="Recovery heartbeat" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="1" Grid.Column="1" x:Name="SongOverlayHeartbeatBox" Minimum="15" Maximum="300" Increment="5" FormatString="0 seconds" HorizontalAlignment="Left" Width="160" />
<TextBlock Grid.Row="2" Text="Seek sensitivity" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="2" Grid.Column="1" x:Name="SongOverlaySeekBox" Minimum="500" Maximum="10000" Increment="250" FormatString="0 ms" HorizontalAlignment="Left" Width="160" />
</Grid>
<CheckBox x:Name="SongOverlayCoverToggle" Content="Send cover art only with track metadata" />
<CheckBox x:Name="SongOverlaySearchLinkToggle" Content="Use a Spotify search link when an exact song link is unavailable" />
</StackPanel>
<Border Classes="card">
<StackPanel Spacing="12">
<StackPanel Spacing="4">
<TextBlock Text="SPOTIFY-SPECIFIC" Classes="eyebrow" />
<TextBlock Text="Optional metadata enrichment" Classes="sectionTitle" />
<TextBlock Text="Windows normally supplies title, artist, album, timeline and artwork. Connecting your own Spotify application adds an exact link, release year and official artwork when available. Spotify is contacted only when track metadata changes or needs recovery." Classes="muted" />
</StackPanel>
<TextBox x:Name="SongOverlaySpotifyClientIdBox" PlaceholderText="Spotify application Client ID" />
<StackPanel Orientation="Horizontal" Spacing="10">
<Button x:Name="ConnectSongOverlaySpotifyButton" Classes="secondary" Content="Connect Spotify metadata" />
<Button x:Name="DisconnectSongOverlaySpotifyButton" Classes="secondary" Content="Disconnect" />
</StackPanel>
<TextBlock x:Name="SongOverlaySpotifyStatus" Text="Not connected. Core playback detection still works through Windows." Classes="muted" />
</StackPanel>
</Border>
<StackPanel Orientation="Horizontal" Spacing="10">
<Button x:Name="SaveSongOverlayButton" Classes="primary" Content="Save Song Overlay" />
<Button x:Name="SendSongOverlayStateButton" Classes="secondary" Content="Send current song now" />
</StackPanel>
<TextBlock x:Name="SongOverlayFeedback" Classes="muted" />
</StackPanel>
</ScrollViewer>
<ScrollViewer x:Name="LumiOverlayPage" IsVisible="False">
<StackPanel Margin="44,38,52,48" MaxWidth="820" HorizontalAlignment="Left" Spacing="24">
<StackPanel Spacing="6">
<TextBlock Text="NATIVE MONITOR OVERLAY" Classes="eyebrow" />
<TextBlock Text="Lumi Overlay" Classes="pageTitle" />
<TextBlock Text="Show Lumi's normalized chat and stream events directly on one Windows monitor. This is separate from browser-source overlays and never receives provider credentials." Classes="muted" FontSize="15" TextWrapping="Wrap" />
<TextBlock Text="Changes on this page stay as an unsaved draft until you select Save Lumi Overlay. Live feed refreshes will not replace the draft." Classes="muted" FontSize="12" TextWrapping="Wrap" />
</StackPanel>
<Grid ColumnDefinitions="*,*" ColumnSpacing="12">
<Border Classes="soft">
<StackPanel Spacing="4">
<TextBlock Text="Feed" Classes="muted" FontSize="11" />
<TextBlock x:Name="LumiOverlayFeedStatus" Text="Starting" FontWeight="SemiBold" />
<TextBlock x:Name="LumiOverlayFeedDetail" Text="Loading the native overlay…" Classes="muted" TextWrapping="Wrap" />
<TextBlock x:Name="LumiOverlayProviderStatus" Text="Provider status will appear after Lumi connects." Classes="muted" FontSize="11" TextWrapping="Wrap" />
</StackPanel>
</Border>
<Border Grid.Column="1" Classes="soft">
<StackPanel Spacing="4">
<TextBlock Text="Capture exclusion" Classes="muted" FontSize="11" />
<TextBlock x:Name="LumiOverlayCaptureStatus" Text="Pending" FontWeight="SemiBold" />
<TextBlock x:Name="LumiOverlayCaptureDetail" Text="Waiting for the overlay window." Classes="muted" TextWrapping="Wrap" />
</StackPanel>
</Border>
</Grid>
<TextBlock x:Name="LumiOverlayMonitorWarning" Foreground="{DynamicResource LumiWarningBrush}" TextWrapping="Wrap" IsVisible="False" />
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock Text="Display &amp; visibility" Classes="sectionTitle" />
<Grid ColumnDefinitions="180,*" RowDefinitions="Auto,Auto,Auto,Auto" RowSpacing="10" ColumnSpacing="12">
<TextBlock Text="Monitor" Classes="muted" VerticalAlignment="Center" />
<ComboBox Grid.Column="1" x:Name="LumiOverlayMonitorPicker" />
<TextBlock Grid.Row="1" Text="Visibility" Classes="muted" VerticalAlignment="Center" />
<ComboBox Grid.Row="1" Grid.Column="1" x:Name="LumiOverlayVisibilityPicker" />
<TextBlock Grid.Row="2" Text="Layout" Classes="muted" VerticalAlignment="Center" />
<ComboBox Grid.Row="2" Grid.Column="1" x:Name="LumiOverlayLayoutPicker" />
<TextBlock Grid.Row="3" x:Name="LumiOverlayChatShareLabel" Text="Chat share in divided layout" Classes="muted" VerticalAlignment="Center" />
<StackPanel Grid.Row="3" Grid.Column="1" x:Name="LumiOverlayChatShareEditor" Orientation="Horizontal" Spacing="7">
<NumericUpDown x:Name="LumiOverlayChatPercentageBox" Minimum="10" Maximum="90" Increment="5" FormatString="0" HorizontalAlignment="Left" Width="112" />
<TextBlock Text="%" Classes="muted" VerticalAlignment="Center" />
</StackPanel>
</Grid>
<CheckBox x:Name="LumiOverlayEnabledToggle" Content="Run Lumi Overlay automatically with Companion" />
<TextBlock x:Name="LumiOverlayVisibilityState" Text="Current display: checking…" Classes="muted" FontSize="12" TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal" Spacing="10">
<Button x:Name="LumiOverlayPreviewButton" Classes="secondary" Content="Start sample preview" />
<Button x:Name="LumiOverlayVisibilityButton" Classes="secondary" Content="Show temporarily" />
</StackPanel>
</StackPanel>
</Border>
<Grid ColumnDefinitions="*,*" ColumnSpacing="16">
<StackPanel Spacing="10">
<TextBlock Text="Sources" Classes="sectionTitle" />
<TextBlock Text="Only checked sources are sent over this device feed. Saved unavailable sources stay listed." Classes="muted" FontSize="12" TextWrapping="Wrap" />
<Border Classes="soft"><StackPanel x:Name="LumiOverlaySourcesPanel" Spacing="8"><TextBlock Text="Waiting for Lumi…" Classes="muted" /></StackPanel></Border>
</StackPanel>
<StackPanel Grid.Column="1" Spacing="10">
<TextBlock Text="Event types" Classes="sectionTitle" />
<TextBlock Text="Chat is controlled by Sources. Choose which non-chat events should appear." Classes="muted" FontSize="12" TextWrapping="Wrap" />
<Border Classes="soft"><StackPanel x:Name="LumiOverlayEventsPanel" Spacing="8"><TextBlock Text="Waiting for Lumi…" Classes="muted" /></StackPanel></Border>
</StackPanel>
</Grid>
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock Text="Container styling &amp; lifecycle" Classes="sectionTitle" />
<ComboBox x:Name="LumiOverlayContainerPicker" HorizontalAlignment="Left" Width="240">
<ComboBoxItem x:Name="LumiOverlayUnifiedContainerOption" Content="Unified container" Tag="unified" />
<ComboBoxItem x:Name="LumiOverlayChatContainerOption" Content="Chat container" Tag="chat" />
<ComboBoxItem x:Name="LumiOverlayEventContainerOption" Content="Event container" Tag="events" />
</ComboBox>
<Grid ColumnDefinitions="170,180,170,180" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto" RowSpacing="10" ColumnSpacing="10">
<TextBlock Text="Anchor" Classes="muted" VerticalAlignment="Center" />
<ComboBox Grid.Column="1" x:Name="LumiOverlayAnchorPicker" />
<TextBlock Grid.Column="2" Text="Newest items" Classes="muted" VerticalAlignment="Center" />
<ComboBox Grid.Column="3" x:Name="LumiOverlayDirectionPicker" />
<TextBlock Grid.Row="1" Text="Width (physical px)" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="1" Grid.Column="1" x:Name="LumiOverlayWidthBox" Minimum="160" Maximum="16000" />
<TextBlock Grid.Row="1" Grid.Column="2" Text="Height (physical px)" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="1" Grid.Column="3" x:Name="LumiOverlayHeightBox" Minimum="100" Maximum="16000" />
<TextBlock Grid.Row="2" Text="Card RGBA" Classes="muted" VerticalAlignment="Center" />
<TextBox Grid.Row="2" Grid.Column="1" x:Name="LumiOverlayCardColorBox" />
<TextBlock Grid.Row="2" Grid.Column="2" Text="Text RGB" Classes="muted" VerticalAlignment="Center" />
<TextBox Grid.Row="2" Grid.Column="3" x:Name="LumiOverlayTextColorBox" />
<TextBlock Grid.Row="3" Text="Font" Classes="muted" VerticalAlignment="Center" />
<ComboBox Grid.Row="3" Grid.Column="1" x:Name="LumiOverlayFontBox" />
<TextBlock Grid.Row="3" Grid.Column="2" Text="Font size" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="3" Grid.Column="3" x:Name="LumiOverlayFontSizeBox" Minimum="8" Maximum="120" />
<TextBlock Grid.Row="4" Text="Entry animation" Classes="muted" VerticalAlignment="Center" />
<ComboBox Grid.Row="4" Grid.Column="1" x:Name="LumiOverlayEntryPicker" />
<TextBlock Grid.Row="4" Grid.Column="2" Text="Exit animation" Classes="muted" VerticalAlignment="Center" />
<ComboBox Grid.Row="4" Grid.Column="3" x:Name="LumiOverlayExitPicker" />
<TextBlock Grid.Row="5" Text="Lifetime (ms)" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="5" Grid.Column="1" x:Name="LumiOverlayLifetimeBox" Minimum="1000" Maximum="300000" Increment="1000" />
<TextBlock Grid.Row="5" Grid.Column="2" Text="Queue limit" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="5" Grid.Column="3" x:Name="LumiOverlayQueueBox" Minimum="1" Maximum="100" />
</Grid>
<StackPanel Orientation="Horizontal" Spacing="14">
<CheckBox x:Name="LumiOverlayAvatarToggle" Content="Avatar" />
<CheckBox x:Name="LumiOverlayUsernameToggle" Content="Username" />
<CheckBox x:Name="LumiOverlayPlatformToggle" Content="Platform identity" />
</StackPanel>
<Expander Header="Position, shadow &amp; animation details">
<StackPanel Spacing="12" Margin="0,12,0,0">
<Grid ColumnDefinitions="130,120,130,120" RowDefinitions="Auto,Auto,Auto" RowSpacing="8" ColumnSpacing="10">
<TextBlock Text="Top padding" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Column="1" x:Name="LumiOverlayPaddingTopBox" Minimum="0" Maximum="4000" />
<TextBlock Grid.Column="2" Text="Right padding" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Column="3" x:Name="LumiOverlayPaddingRightBox" Minimum="0" Maximum="4000" />
<TextBlock Grid.Row="1" Text="Bottom padding" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="1" Grid.Column="1" x:Name="LumiOverlayPaddingBottomBox" Minimum="0" Maximum="4000" />
<TextBlock Grid.Row="1" Grid.Column="2" Text="Left padding" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="1" Grid.Column="3" x:Name="LumiOverlayPaddingLeftBox" Minimum="0" Maximum="4000" />
<TextBlock Grid.Row="2" Text="Horizontal align" Classes="muted" VerticalAlignment="Center" />
<ComboBox Grid.Row="2" Grid.Column="1" x:Name="LumiOverlayHorizontalAlignPicker" />
<TextBlock Grid.Row="2" Grid.Column="2" Text="Vertical align" Classes="muted" VerticalAlignment="Center" />
<ComboBox Grid.Row="2" Grid.Column="3" x:Name="LumiOverlayVerticalAlignPicker" />
</Grid>
<Grid ColumnDefinitions="130,120,130,120" RowDefinitions="Auto,Auto" RowSpacing="8" ColumnSpacing="10">
<TextBlock Text="Shadow RGBA" Classes="muted" VerticalAlignment="Center" />
<TextBox Grid.Column="1" x:Name="LumiOverlayShadowColorBox" />
<TextBlock Grid.Column="2" Text="Shadow blur" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Column="3" x:Name="LumiOverlayShadowBlurBox" Minimum="0" Maximum="40" />
<TextBlock Grid.Row="1" Text="Shadow X / Y" Classes="muted" VerticalAlignment="Center" />
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" Spacing="6">
<NumericUpDown x:Name="LumiOverlayShadowXBox" Minimum="-40" Maximum="40" Width="56" />
<NumericUpDown x:Name="LumiOverlayShadowYBox" Minimum="-40" Maximum="40" Width="56" />
</StackPanel>
<TextBlock Grid.Row="1" Grid.Column="2" Text="Easing" Classes="muted" VerticalAlignment="Center" />
<ComboBox Grid.Row="1" Grid.Column="3" x:Name="LumiOverlayEasingPicker" />
</Grid>
<Grid ColumnDefinitions="130,120,130,120" RowDefinitions="Auto,Auto" RowSpacing="8" ColumnSpacing="10">
<TextBlock Text="Entry duration" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Column="1" x:Name="LumiOverlayEntryDurationBox" Minimum="0" Maximum="5000" Increment="20" FormatString="0 ms" />
<TextBlock Grid.Column="2" Text="Exit duration" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Column="3" x:Name="LumiOverlayExitDurationBox" Minimum="0" Maximum="5000" Increment="20" FormatString="0 ms" />
<TextBlock Grid.Row="1" Text="Slide distance" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="1" Grid.Column="1" x:Name="LumiOverlaySlideDistanceBox" Minimum="0" Maximum="1000" />
<TextBlock Grid.Row="1" Grid.Column="2" Text="Scale amount" Classes="muted" VerticalAlignment="Center" />
<NumericUpDown Grid.Row="1" Grid.Column="3" x:Name="LumiOverlayScaleAmountBox" Minimum="0.1" Maximum="2" Increment="0.05" FormatString="0.00" />
</Grid>
</StackPanel>
</Expander>
<TextBlock Text="Disabled padding edges are always stored as zero. Width and height are physical pixels and clamp to the selected monitor." Classes="muted" FontSize="12" TextWrapping="Wrap" />
</StackPanel>
</Border>
<Border Classes="soft">
<StackPanel Spacing="12">
<TextBlock Text="Global hotkeys" Classes="sectionTitle" />
<Grid ColumnDefinitions="180,*" RowDefinitions="Auto,Auto" RowSpacing="10">
<TextBlock Text="Visibility override" Classes="muted" />
<TextBox Grid.Column="1" x:Name="LumiOverlayVisibilityHotkeyBox" PlaceholderText="Ctrl+Alt+L" />
<TextBlock Grid.Row="1" Text="Preview override" Classes="muted" />
<TextBox Grid.Row="1" Grid.Column="1" x:Name="LumiOverlayPreviewHotkeyBox" PlaceholderText="Ctrl+Alt+P" />
</Grid>
<TextBlock x:Name="LumiOverlayHotkeyStatus" Text="Hotkeys are not registered yet." Classes="muted" FontSize="12" TextWrapping="Wrap" />
</StackPanel>
</Border>
<StackPanel Orientation="Horizontal" Spacing="10">
<Button x:Name="SaveLumiOverlayButton" Classes="primary" Content="Save Lumi Overlay" />
</StackPanel>
<TextBlock x:Name="LumiOverlayFeedback" Classes="muted" TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
<ScrollViewer x:Name="ConnectionPage" IsVisible="False">
<StackPanel Margin="44,38,52,48" MaxWidth="760" HorizontalAlignment="Left" Spacing="24">
<StackPanel Spacing="6">
<TextBlock Text="DEVICE" Classes="eyebrow" />
<TextBlock Text="Connection &amp; device" Classes="pageTitle" />
<TextBlock Text="The device credential is protected for your Windows account and never shared with OBS." Classes="muted" FontSize="15" />
</StackPanel>
<Grid ColumnDefinitions="150,*" RowDefinitions="Auto,Auto,Auto" RowSpacing="12">
<TextBlock Text="Computer" Classes="muted" /><TextBlock Grid.Column="1" x:Name="DeviceNameText" Text="Not paired" FontWeight="SemiBold" />
<TextBlock Grid.Row="1" Text="Lumi host" Classes="muted" /><TextBlock Grid.Row="1" Grid.Column="1" x:Name="HostText" Text="—" />
<TextBlock Grid.Row="2" Text="Last connected" Classes="muted" /><TextBlock Grid.Row="2" Grid.Column="1" x:Name="LastConnectedText" Text="Never" />
</Grid>
<StackPanel Orientation="Horizontal" Spacing="10">
<Button x:Name="ReconnectButton" Classes="secondary" Content="Retry connection" />
<Button x:Name="PairButton" Classes="secondary" Content="Choose pairing package" />
<Button x:Name="OpenWebButton" Classes="secondary" Content="Open Lumi WebUI" />
</StackPanel>
<Border x:Name="ConnectionBridgeUpdatePanel" Background="#FFF2D6" BorderBrush="#D58A1F" BorderThickness="1" CornerRadius="12" Padding="18" IsVisible="False">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="20">
<StackPanel Spacing="4">
<TextBlock Text="OBS Bridge update required" FontWeight="Bold" Foreground="#764600" />
<TextBlock x:Name="ConnectionBridgeUpdateDetail" Text="Close OBS before updating the managed integration." TextWrapping="Wrap" Foreground="#764600" />
</StackPanel>
<Button Grid.Column="1" x:Name="ConnectionUpdateBridgeButton" Classes="primary" Content="Update OBS Bridge" VerticalAlignment="Center" />
</Grid>
</Border>
<Border Classes="soft">
<StackPanel Spacing="10">
<TextBlock Text="Managed OBS integration" FontWeight="SemiBold" />
<TextBlock x:Name="BridgeStatusText" Text="Checking the bundled integration…" Classes="muted" />
<StackPanel Orientation="Horizontal" Spacing="10">
<Button x:Name="InstallBridgeButton" Classes="primary" Content="Install integration" />
<Button x:Name="RemoveBridgeButton" Classes="secondary" Content="Remove integration" />
</StackPanel>
<TextBlock Text="Close OBS before install, repair, or removal. Restart OBS afterward so it can load the verified plugin." Classes="muted" FontSize="12" />
</StackPanel>
</Border>
<Border Classes="soft">
<StackPanel Spacing="8">
<TextBlock Text="Remove this device" FontWeight="SemiBold" />
<TextBlock Text="Removes the local credential. You can also revoke this computer from Lumi." Classes="muted" />
<Button x:Name="ForgetButton" Classes="secondary" Content="Forget this device" HorizontalAlignment="Left" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<ScrollViewer x:Name="LogsPage" IsVisible="False">
<StackPanel Margin="44,38,52,48" MaxWidth="760" HorizontalAlignment="Left" Spacing="24">
<StackPanel Spacing="6">
<TextBlock Text="DIAGNOSTICS" Classes="eyebrow" />
<TextBlock Text="Logs &amp; diagnostics" Classes="pageTitle" />
<TextBlock Text="Short-lived diagnostic logs help explain connection and delivery failures. Raw audio is never written to disk." Classes="muted" FontSize="15" />
</StackPanel>
<Button x:Name="OpenLogsButton" Classes="secondary" Content="Open logs folder" HorizontalAlignment="Left" />
<Border Background="#182026" CornerRadius="12" Padding="16" MinHeight="230">
<StackPanel x:Name="RecentLogsPanel" Spacing="6">
<TextBlock Text="Waiting for companion events…" Foreground="#CFDADD" FontFamily="Consolas" FontSize="12" />
</StackPanel>
</Border>
<TextBlock Text="Logs are retained for 7 days and capped at 256 MiB. Caption text may appear in diagnostics during testing or troubleshooting." Classes="muted" FontSize="12" />
</StackPanel>
</ScrollViewer>
<ScrollViewer x:Name="SettingsPage" IsVisible="False">
<StackPanel Margin="44,38,52,48" MaxWidth="760" HorizontalAlignment="Left" Spacing="25">
<StackPanel Spacing="6">
<TextBlock Text="PREFERENCES" Classes="eyebrow" />
<TextBlock Text="Settings" Classes="pageTitle" />
<TextBlock Text="Only controls useful on the streaming computer live here. Model and server administration stay in Lumi." Classes="muted" FontSize="15" />
</StackPanel>
<StackPanel Spacing="16">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="24">
<StackPanel Spacing="3"><TextBlock Text="Start with Windows" FontWeight="SemiBold" /><TextBlock Text="Keep Lumi Companion ready in the notification area after sign-in." Classes="muted" /></StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="AutoStartToggle" OffContent="Off" OnContent="On" />
</Grid>
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="24">
<StackPanel Spacing="3"><TextBlock Text="Start companion features with OBS" FontWeight="SemiBold" /><TextBlock Text="Prepare selected features when OBS becomes available." Classes="muted" /></StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="StartWithObsToggle" OffContent="Off" OnContent="On" />
</Grid>
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="24">
<StackPanel Spacing="3"><TextBlock Text="Advanced mode" FontWeight="SemiBold" /><TextBlock Text="Reveal protocol and component details useful for troubleshooting." Classes="muted" /></StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="AdvancedToggle" OffContent="Off" OnContent="On" />
</Grid>
</StackPanel>
<Border x:Name="AdvancedPanel" Classes="soft" IsVisible="False">
<StackPanel Spacing="6">
<TextBlock Text="Advanced details" FontWeight="SemiBold" />
<TextBlock Text="Protocol v1 · bounded in-memory audio · secure WebSocket transport · server-hosted inference" Classes="muted" />
</StackPanel>
</Border>
<Border Classes="soft">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="20">
<StackPanel Spacing="4"><TextBlock Text="Companion updates" FontWeight="SemiBold" /><TextBlock x:Name="UpdateStatusText" Text="Checking for updates…" Classes="muted" /><TextBlock Text="Current version" Classes="muted" FontSize="11" /><TextBlock x:Name="CurrentVersionText" Text="—" FontSize="12" /></StackPanel>
<Button Grid.Column="1" x:Name="CheckUpdateButton" Classes="secondary" Content="Check now" VerticalAlignment="Center" />
</Grid>
</Border>
<Button x:Name="SaveSettingsButton" Classes="primary" Content="Save preferences" HorizontalAlignment="Left" />
<TextBlock x:Name="SettingsFeedback" Classes="muted" />
</StackPanel>
</ScrollViewer>
</Grid>
</Grid>
</Window>