Editor/HumanoidMocap/PhoneUploadDialog.cs
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using Editor;
using Sandbox;
using HumanoidMocap.Editor.PhoneQr;

namespace HumanoidMocap.Editor;

public sealed class PhoneUploadDialog : Dialog
{
    PhoneUploadServer server;
    readonly Label status;
    readonly QrWidget qr;
    readonly LineEdit address;
    readonly Action<string> received;
    IPAddress selected;
    internal string PairingUrl=>server?.PairingUrl;
    internal Rect QrScreenRect=>qr.ScreenRect;
    public PhoneUploadDialog(Widget parent,Action<string> received):base(parent)
    {
        this.received=received;Window.WindowTitle="Receive from Phone";Window.SetWindowIcon("qr_code_2");
        Window.MinimumSize=new Vector2(440,680);
        Layout=Layout.Column();Layout.Margin=20;Layout.Spacing=12;
        SetStyles($"background-color: {Theme.WindowBackground.Hex}; color: {Theme.Text.Hex};");
        Layout.Add(new Label.Subtitle("Receive from Phone"){Alignment=TextFlag.Center});
        var subtitle=Layout.Add(new Label("Scan once · upload videos for one hour",this){Alignment=TextFlag.Center});
        subtitle.SetStyles($"color: {Theme.TextLight.Hex};");
        var networks=PhoneNetwork.Discover();selected=networks[0].Address;
        var network=Layout.Add(new ComboBox(this));
        network.ToolTip="Choose the Wi-Fi or Ethernet connection shared with your phone. Virtual adapters such as WSL are usually unreachable from phones.";
        foreach(var connection in networks)network.AddItem(connection.Label,"wifi",()=>{selected=connection.Address;Start();},selected:connection.Address.Equals(selected));
        // Fill the available width. The painter centers the square and mark on both axes.
        qr=Layout.Add(new QrWidget(this){MinimumSize=new Vector2(300,300)},1);
        address=Layout.Add(new LineEdit(this));address.ReadOnly=true;
        status=Layout.Add(new Label("",this){Alignment=TextFlag.Center});
        status.SetStyles($"color: {Theme.Green.Hex};");
        var help=Layout.Add(new Label("Connect your phone to the same router as this PC.\nUse Wi-Fi or Ethernet above, not a WSL/VPN adapter.\nIf the phone page never loads, allow s&box through\nWindows Firewall for private and public networks.\nKeep this window open to receive more videos.",this){Alignment=TextFlag.Center,WordWrap=true});
        help.SetStyles($"color: {Theme.TextLight.Hex};");
        var row=Layout.AddRow();row.Spacing=8;
        var renew=row.Add(new Button.Primary("New one-hour pairing"){Icon="refresh"},1);renew.Clicked=Start;
        var stop=row.Add(new Button("Disconnect phones","link_off"));stop.Clicked=()=>{server?.Dispose();server=null;qr.Code=null;qr.Update();status.Text="Disconnected. Existing phone links no longer work.";address.Text="";};
        Start();Window.Size=new Vector2(500,700);
    }
    void Start()
    {
        server?.Dispose();
        try
        {
            var folder=Path.Combine(Project.Current.GetAssetsPath(),"humanoid_mocap","uploads");
            server=new PhoneUploadServer(selected,folder);
            server.Received=path=>_=NotifyAsync(path);
            server.Error=message=>_=StatusAsync(message);
            server.Progress=(done,total)=>_=StatusAsync($"Receiving video · {100*done/total}%");
            server.ThemeCss=$":root{{--window:{Theme.WindowBackground.Hex};--surface:{Theme.WidgetBackground.Hex};--border:{Theme.Border.Hex};--text:{Theme.Text.Hex};--muted:{Theme.TextLight.Hex};--primary:{Theme.Blue.Hex};}}";
            qr.Code=PhoneQrPresentation.Create(server.PairingUrl);qr.Update();address.Text=server.PairingUrl;
            var connection=PhoneNetwork.Discover().FirstOrDefault(n=>n.Address.Equals(selected));
            status.Text=connection?.LocalOnly==true?"This address cannot receive uploads from a phone. Connect to Wi-Fi or Ethernet."
                :connection?.Virtual==true?"Virtual adapter selected. Choose Wi-Fi or Ethernet if your phone cannot connect."
                :"Ready · one-hour pairing. Uploads stay on this PC.";
        }
        catch(Exception e){status.Text=e.Message;}
    }
    async System.Threading.Tasks.Task NotifyAsync(string path)
    {await EditorPipeline.SwitchToMainThread();if(!this.IsValid())return;status.Text="Received "+Path.GetFileName(path);received(path);}
    async System.Threading.Tasks.Task StatusAsync(string message)
    {await EditorPipeline.SwitchToMainThread();if(this.IsValid())status.Text=message;}
    public override void OnDestroyed(){server?.Dispose();base.OnDestroyed();}
    sealed class QrWidget : Widget
    {
        public QrCode Code { get; set; }
        public QrWidget(Widget parent):base(parent){}
        protected override void OnPaint()=>PhoneQrPainter.Draw(Code,(int)Width,(int)Height);
    }
}