using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Platform.Storage; using Avalonia.Threading; using Lumi.Companion.Abstractions; using Lumi.Companion.SongOverlay; namespace Lumi.Companion.App; public partial class MainWindow : Window { private readonly CompanionRuntime _runtime; private readonly CompanionSettingsStore _settings; private readonly SongOverlayRuntime _songOverlay; private readonly IReadOnlyList _plugins; private readonly Dictionary _navigationButtons = []; private bool _allowExit; private bool _renderingSources; private bool _renderingSongOverlay; public MainWindow() : this(CreateDefaultServices()) { } private MainWindow((CompanionRuntime Runtime, CompanionSettingsStore Settings, SongOverlayRuntime SongOverlay, IReadOnlyList Plugins) services) : this(services.Runtime, services.Settings, services.SongOverlay, services.Plugins) { } public MainWindow(CompanionRuntime runtime, CompanionSettingsStore settings, SongOverlayRuntime songOverlay, IReadOnlyList plugins) { _runtime = runtime; _settings = settings; _songOverlay = songOverlay; _plugins = plugins; InitializeComponent(); BuildPluginNavigation(); WireActions(); RenderState(runtime.State); RenderTestStages(runtime.TestStages); RenderBenchmark(runtime.Benchmark); RenderSongOverlay(); Closing += OnClosing; runtime.StateChanged += state => Dispatcher.UIThread.Post(() => { RenderState(state); RenderSongOverlay(); }); runtime.TestStagesChanged += stages => Dispatcher.UIThread.Post(() => RenderTestStages(stages)); runtime.ObsSourcesChanged += sources => Dispatcher.UIThread.Post(() => RenderSources(sources)); runtime.BenchmarkChanged += benchmark => Dispatcher.UIThread.Post(() => RenderBenchmark(benchmark)); runtime.VoiceLevelChanged += level => Dispatcher.UIThread.Post(() => RenderVoiceLevel(level)); runtime.CaptionReceived += (text, simulated) => Dispatcher.UIThread.Post(() => { if (simulated) SimulatedCaption.Text = text; }); runtime.LogAdded += line => Dispatcher.UIThread.Post(() => AddLog(line)); settings.Changed += value => Dispatcher.UIThread.Post(() => RenderSettings(value)); songOverlay.Changed += () => Dispatcher.UIThread.Post(RenderSongOverlay); } private void BuildPluginNavigation() { PluginNavigation.Children.Clear(); foreach (var plugin in _plugins.OrderBy(item => item.Descriptor.Order).ThenBy(item => item.Descriptor.Name, StringComparer.OrdinalIgnoreCase)) { var children = new StackPanel { Spacing = 2, Margin = new Thickness(8, 3, 0, 6) }; foreach (var page in plugin.Pages.OrderBy(item => item.Order)) { if (!Enum.TryParse(page.Key, out var parsed)) continue; var button = new Button { Content = page.Label, Tag = page.Key, HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Left }; button.Classes.Add("nav"); button.Click += OnNavigate; children.Children.Add(button); _navigationButtons[parsed] = button; } var expander = new Expander { Header = new TextBlock { Text = plugin.Descriptor.Name, FontWeight = FontWeight.SemiBold }, IsExpanded = true, Content = children, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch }; expander.Classes.Add("pluginRoot"); PluginNavigation.Children.Add(expander); } _navigationButtons[CompanionPage.Overview] = OverviewNav; _navigationButtons[CompanionPage.Connection] = ConnectionNav; _navigationButtons[CompanionPage.Logs] = LogsNav; _navigationButtons[CompanionPage.Settings] = SettingsNav; } private void WireActions() { OverviewNav.Click += OnNavigate; foreach (var button in TechnicalNavigation.Children.OfType