Editor/AccountBar.cs
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Editor;
using Sandbox;
namespace HumanoidMocapX.EditorTools;
/// <summary>The strip across the top of the X window. With a provider: avatar, name, status pills,
/// account ID and limits, then icon buttons for the queue, adding videos and sign-out. On a processing
/// machine: its SSH address. On this computer: the GPU. Before setup: a pointer to settings. Missing
/// names and pictures show as not provided; account details and setup live in Mocap adjustments › Processing.</summary>
sealed class AccountBar : Widget
{
readonly HostingSettings settings;
readonly QueueTracker queue;
readonly AccountAvatar avatar;
readonly Label name, details, problem;
readonly Layout pills;
readonly Button signIn;
readonly BadgeButton queueButton;
readonly IconButton add, signOut;
CancellationTokenSource pending;
/// <summary>Provider mode when <paramref name="queue"/> is set, otherwise the processing machine
/// or this computer, or a pointer to settings while processing is not <paramref name="configured"/>.</summary>
internal AccountBar(Widget parent, HostingSettings settings, SshSettings machine, QueueTracker queue, bool configured, Action openSetup) : base(parent)
{
this.settings = settings; this.queue = queue;
Layout = Layout.Row(); Layout.Margin = new Sandbox.UI.Margin(12, 10, 12, 10); Layout.Spacing = 10;
avatar = Layout.Add(new AccountAvatar(this, 34));
var text = Layout.AddColumn(1); text.Spacing = 2;
var top = text.AddRow(); top.Spacing = 8;
name = top.Add(Ui.Bold(new Label("", this), 14));
pills = top.AddRow(); pills.Spacing = 6;
top.AddStretchCell();
details = text.Add(Ui.Muted(new Label("", this), small: true));
problem = Layout.Add(Ui.Error(this));
if (!configured)
{
avatar.ShowIcon("settings");
name.Text = "Not set up";
details.Text = "Choose where videos are processed: settings (top right) › Processing › Setup.";
return;
}
if (queue is null && machine.Enabled)
{
avatar.ShowIcon("dns");
name.Text = "Processing machine";
details.Text = $"{machine.Address} over SSH · videos are copied there for processing";
return;
}
if (queue is null)
{
avatar.ShowIcon("memory");
name.Text = "This computer";
var gpu = Sandbox.Engine.SystemInfo.Gpu is { Length: > 0 } gpuName && gpuName != "unset" ? gpuName : "This GPU";
details.Text = $"{gpu} · up to {FlowVideoProcessor.LaneCapacity()} frames at once · videos never leave this computer";
return;
}
signIn = Layout.Add(Ui.Primary("Sign in", "login", () => _ = SignInAsync(openSetup)));
queueButton = Layout.Add(new BadgeButton(this, "format_list_bulleted", OpenQueue, "Queue: your jobs on the provider"));
add = Layout.Add(Ui.Icon(this, "playlist_add", () => QueuePopup.AddVideos(queue), "Add videos to the queue"));
signOut = Layout.Add(Ui.Icon(this, "logout", () => _ = SignOutAsync(), "Sign out"));
settings.Changed += Refresh; queue.Changed += Refresh;
Refresh();
}
void OpenQueue()
{
var popup = new QueuePopup(this, queue);
popup.OpenAt(queueButton.ScreenRect.BottomLeft + new Vector2(queueButton.Width - popup.FixedWidth, 6));
}
void Refresh()
{
if (!this.IsValid()) return;
var profile = settings.SignedIn ? settings.Profile : null;
var label = EditorCookie.Get(ProcessingSetup.ProviderLabelCookie, "");
signIn.Visible = profile is null; signIn.Text = label.Length > 0 ? "Sign in with " + label : "Sign in";
queueButton.Visible = add.Visible = signOut.Visible = profile is not null;
queueButton.Count = queue.Active;
pills.Clear(true);
if (profile is null)
{
name.Text = "Signed out"; details.Text = Host(); avatar.ShowIcon("person");
return;
}
var user = profile.User;
name.Text = Clip(user.Name ?? user.Username, 32) ?? "Name not provided";
var (status, color) = profile switch
{
{ Admin: true } => ("Admin", Ui.Accent),
{ CanProcess: true } => ("Whitelisted", Ui.Accent),
{ InGuild: false } => ("Join the Discord server", Theme.Yellow),
_ => ("Not whitelisted", Theme.Red)
};
pills.Add(new Pill(this, status, color));
if (profile.Limits is { Priority: true }) pills.Add(new Pill(this, "Priority", Theme.Yellow));
details.Text = $"{Clip(user.Id, 40)} · {Host()}" + (profile.Limits is { } limit ? " · " + Describe(limit) : "");
details.ToolTip = user.Id + ": your account ID on this provider. Give it to the provider to be whitelisted. Click to copy.";
avatar.ShowAccount(user.Avatar, user.Name ?? user.Username);
}
protected override void OnMouseClick(MouseEvent e)
{
base.OnMouseClick(e);
// The account ID line copies the ID, so users can send it to the provider.
if (e.LeftMouseButton && details.IsUnderMouse && settings.Profile?.User.Id is { } id) EditorUtility.Clipboard.Copy(id);
}
static string Describe(HostLimits limit)
=> (limit.MaximumClipSeconds is { } clip ? $"clips up to {HostedCapture.Duration((int)clip)}" : "clips up to the provider's limit")
+ $" · uploads up to {limit.MaximumUploadBytes / (1024 * 1024)} MiB";
string Host()
{
try { return settings.Endpoint().Host; } catch (ArgumentException) { return "No provider address"; }
}
static string Clip(string text, int length) => text is { } value && value.Length > length ? value[..(length - 1)] + "…" : text;
async Task SignInAsync(Action openSetup)
{
if (pending is not null) return;
if (ProcessingSetup.SavedProvider() is not { } provider) { openSetup(); return; }
using var cancellation = new CancellationTokenSource(); pending = cancellation; var token = cancellation.Token;
Ui.Show(problem, null); signIn.Enabled = false; signIn.Text = "Finish in your browser…";
try { await ProcessingSetup.SignInAsync(settings, provider, token); }
catch (Exception failure) when (ProcessingSetup.IsSignInFailure(failure))
{
await EditorPipeline.SwitchToMainThread();
if (!token.IsCancellationRequested) Ui.Show(problem, failure is OperationCanceledException ? "Sign-in timed out. Try again." : failure.Message);
}
finally
{
await EditorPipeline.SwitchToMainThread();
pending = null;
if (signIn.IsValid()) { signIn.Enabled = true; Refresh(); }
}
}
async Task SignOutAsync()
{
using var client = new HostSignIn();
await client.SignOutAsync(settings);
}
public override void OnDestroyed()
{
settings.Changed -= Refresh; pending?.Cancel();
if (queue is not null) queue.Changed -= Refresh;
base.OnDestroyed();
}
protected override void OnPaint()
{
Paint.SetPen(Ui.CardEdge, 1);
Paint.DrawLine(new Vector2(0, Height - .5f), new Vector2(Width, Height - .5f));
}
}