Files
windows/PairingWindowHost.cs

132 lines
5.9 KiB
C#

using MusicBridge.Agent.Network;
namespace MusicBridge.Agent;
internal sealed class PairingWindowHost(PairingService pairing, string fingerprint, Func<Uri[]> 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<string>().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));
}
}