using MusicBridge.Agent.Network; namespace MusicBridge.Agent; internal sealed class PairingWindowHost(PairingService pairing, string fingerprint, Func endpoints, bool visible = true) : IDisposable { private readonly object gate = new(); private Thread? thread; private Form? form; private bool disposed; public string? Error { get; private set; } public void Open() { lock (gate) { if (disposed) return; if (thread?.IsAlive == true) { if (form is { IsHandleCreated: true } active) { try { active.BeginInvoke(() => { active.WindowState = FormWindowState.Normal; active.Activate(); }); } catch (InvalidOperationException) { } } return; } thread = new Thread(Run) { IsBackground = true, Name = "MusicBridge QR" }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); } } private void Run() { try { using var window = new Form { Text = "MusicBridge — подключение iPhone", ClientSize = new Size(620, 740), MinimumSize = new Size(440, 560), StartPosition = FormStartPosition.CenterScreen, AutoScaleMode = AutoScaleMode.Dpi, ShowInTaskbar = visible, Opacity = visible ? 1 : 0 }; using var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 4, Padding = new Padding(16) }; layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 58)); layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40)); layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 72)); var title = new Label { Text = "Подключение к компьютеру\nВыбери адрес домашней сети", Dock = DockStyle.Fill }; var selector = new ComboBox { Dock = DockStyle.Fill, DropDownStyle = ComboBoxStyle.DropDownList }; var picture = new PictureBox { Dock = DockStyle.Fill, SizeMode = PictureBoxSizeMode.Zoom, BackColor = Color.White }; var status = new Label { Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleCenter }; layout.Controls.Add(title); layout.Controls.Add(selector); layout.Controls.Add(picture); layout.Controls.Add(status); window.Controls.Add(layout); string? renderedPayload = null; void ClearImage() { var old = picture.Image; picture.Image = null; old?.Dispose(); renderedPayload = null; } void RefreshQr() { try { var choices = endpoints().Select(u => u.GetLeftPart(UriPartial.Authority)).ToArray(); if (!selector.Items.Cast().SequenceEqual(choices)) { var selected = selector.SelectedItem as string; selector.Items.Clear(); selector.Items.AddRange(choices); if (choices.Length > 0) selector.SelectedIndex = Math.Max(0, Array.IndexOf(choices, selected)); } var now = DateTimeOffset.UtcNow; var current = pairing.Current; if (current is null || selector.SelectedItem is not string address) { ClearImage(); status.Text = current is null ? "Сопряжение закрыто. Нажми B в консоли для нового кода." : "Нет адреса домашней сети. Запусти агент с --lan."; return; } var invitation = ConnectionInvitation.Create(new Uri(address), fingerprint, current, now); var payload = invitation.ToQrPayload(); if (renderedPayload != payload) { ClearImage(); using var stream = new MemoryStream(PairingQr.Render(invitation)); using var decoded = Image.FromStream(stream); picture.Image = new Bitmap(decoded); renderedPayload = payload; } status.Text = $"Код: {current.Code} • действует до {current.ExpiresAt.ToLocalTime():HH:mm:ss}\nСканирование предназначено для будущего приложения MusicBridge."; } catch (Exception ex) { ClearImage(); status.Text = $"QR недоступен: {ex.Message}"; } } using var timer = new System.Windows.Forms.Timer { Interval = 500 }; timer.Tick += (_, _) => RefreshQr(); window.Shown += (_, _) => { lock (gate) { if (disposed) { window.Close(); return; } } RefreshQr(); timer.Start(); }; window.FormClosed += (_, _) => { timer.Stop(); ClearImage(); }; lock (gate) { if (disposed) return; form = window; } Application.Run(window); } catch (Exception ex) { Error = ex.Message; } finally { lock (gate) form = null; } } public void Dispose() { Thread? running; lock (gate) { disposed = true; running = thread; if (form is { IsHandleCreated: true } active) { try { active.BeginInvoke(() => active.Close()); } catch (InvalidOperationException) { } } } running?.Join(TimeSpan.FromSeconds(2)); } }