132 lines
5.9 KiB
C#
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));
|
|
}
|
|
}
|