Implement QR pairing window and mDNS announcements; update documentation and tests
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user