SboxSuperuser/UI/AdminMenuRenderer.Views.cs

UI renderer for the admin menu using ImGui. It lays out windows and child panels, displays player lists, selected player details, confirmation dialogs, and recent moderation activity by calling into a Model for data and actions.

NetworkingFile Access
using Sandbox;
using SboxSuperuser.Engine.Vendor.ImGui;

namespace SboxSuperuser.UI;

internal sealed partial class AdminMenuRenderer
{
	private void DrawWindow()
	{
		var size = new Vector2( MathF.Min( Screen.Width - 32, 1040 ), MathF.Min( Screen.Height - 32, 760 ) );
		ImGui.SetNextWindowSize( size );
		ImGui.SetNextWindowPos( new Vector2( (Screen.Width - size.x) / 2, (Screen.Height - size.y) / 2 ) );
		if ( ImGui.Begin( "Superuser", ref Open, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoMove ) )
		{
			ImGui.TextUnformatted( "PLAYER MODERATION" );
			ImGui.SameLine();
			if ( ImGui.Button( "Refresh" ) ) Model.Refresh();
			ImGui.SameLine();
			if ( ImGui.Button( "Close" ) ) Open = false;
			ImGui.TextWrapped( "{0}", Model.Feedback );
			ImGui.Separator();
			var height = MathF.Max( 180, ImGui.GetContentRegionAvail().y - 175 );
			ImGui.BeginChild( "players", new Vector2( MathF.Min( 310, size.x * 0.36f ), height ), true );
			DrawPlayers();
			ImGui.EndChild();
			ImGui.SameLine();
			ImGui.BeginChild( "selected", new Vector2( 0, height ), true );
			DrawSelected();
			ImGui.EndChild();
			ImGui.BeginChild( "activity", new Vector2( 0, 0 ), true );
			DrawActivity();
			ImGui.EndChild();
		}
		ImGui.End();
	}

	private void DrawPlayers()
	{
		ImGui.TextUnformatted( "Online players" );
		ImGui.InputText( "Search", ref Model.Search, 256 );
		if ( ImGui.Button( "Find" ) )
		{
			Model.PlayerPage = 0;
			Model.Refresh();
		}
		var players = Model.Players;
		if ( players is null ) ImGui.TextUnformatted( "Loading..." );
		else if ( players.Status != CommandStatus.Success ) ImGui.TextWrapped( "{0}", players.Message );
		else
		{
			foreach ( var player in players.Players )
			{
				// Display names are text, never ImGui IDs or format strings.
				ImGui.TextWrapped( "{0}", player.Name );
				if ( ImGui.Selectable( player.SteamId, Model.Selected == player.SteamId ) ) Model.Select( player.SteamId );
			}
			if ( Model.PlayerPage > 0 && ImGui.Button( "Previous players" ) )
			{
				Model.PlayerPage--;
				Model.Refresh();
			}
			if ( players.HasMore && ImGui.Button( "More players" ) )
			{
				Model.PlayerPage++;
				Model.Refresh();
			}
		}
		ImGui.Separator();
		ImGui.TextUnformatted( "Offline lookup" );
		ImGui.InputText( "Steam ID", ref Model.Lookup, 17 );
		if ( ImGui.Button( "Look up" ) ) Model.Select( Model.Lookup );
	}

	private void DrawSelected()
	{
		if ( Model.Selected.Length == 0 )
		{
			ImGui.TextWrapped( "Select a player or look up a Steam ID." );
			return;
		}
		ImGui.TextUnformatted( Model.Selected );
		var details = Model.Details;
		if ( details is null ) { ImGui.TextUnformatted( "Loading..." ); return; }
		if ( details.Status != CommandStatus.Success ) { ImGui.TextWrapped( "{0}", details.Message ); return; }
		ImGui.Text( "{0} | {1}", details.Online ? "Online" : "Offline", string.Join( ", ", details.Roles ) );
		ImGui.TextUnformatted( "Active restrictions visible to you" );
		if ( details.Restrictions.Length == 0 ) ImGui.TextUnformatted( "None visible." );
		foreach ( var entry in details.Restrictions ) DrawRecord( entry );
		ImGui.Separator();
		ImGui.InputText( "Reason", ref Model.Reason, 256 );
		ImGui.InputText( "Duration", ref Model.Duration, 32 );
		ImGui.TextUnformatted( "Ban/mute: 30m, 2h, 7d or permanent" );
		ImGui.BeginDisabled( Model.Busy || Model.Confirmation is not null );
		for ( var index = 0; index < details.Actions.Length; index++ )
		{
			if ( index % 3 != 0 ) ImGui.SameLine();
			var action = details.Actions[ index ];
			if ( ImGui.Button( action == AdminAction.Mute ? "Text mute" : action.ToString() ) ) Model.Prepare( action );
		}
		ImGui.EndDisabled();
		if ( details.Actions.Length == 0 ) ImGui.TextWrapped( "No permitted actions for this target." );
		DrawConfirmation();
		ImGui.Separator();
		ImGui.TextUnformatted( "Moderation history" );
		if ( !details.HistoryAllowed ) ImGui.TextWrapped( "Requires admin.players.history." );
		foreach ( var entry in details.History ) DrawRecord( entry );
		if ( Model.HistoryPage > 0 && ImGui.Button( "Previous history" ) )
		{
			Model.HistoryPage--;
			Model.Refresh();
		}
		if ( details.HasMore && ImGui.Button( "More history" ) )
		{
			Model.HistoryPage++;
			Model.Refresh();
		}
	}

	private void DrawConfirmation()
	{
		var confirmation = Model.Confirmation;
		if ( confirmation is null ) return;
		ImGui.Separator();
		ImGui.TextWrapped( "Confirm {0} for {1}", confirmation.Action, confirmation.SteamId );
		ImGui.TextWrapped( "Reason: {0}", confirmation.Reason );
		if ( confirmation.Duration.Length > 0 ) ImGui.Text( "Duration: {0}", confirmation.Duration );
		if ( ImGui.Button( "Confirm action" ) ) Model.Confirm();
		ImGui.SameLine();
		if ( ImGui.Button( "Cancel" ) ) Model.Cancel();
	}

	private static void DrawRecord( AdminModerationRow entry )
	{
		ImGui.TextWrapped( "{0} | {1:u} | {2}", entry.Kind, entry.CreatedUtc, entry.Active ? "active" : "history" );
		ImGui.TextWrapped( "{0}", entry.Reason );
		if ( entry.Kind is "Ban" or "TextMute" ) ImGui.Text( "Expires: {0}", entry.ExpiresUtc?.ToString( "u" ) ?? "permanent" );
		if ( entry.Kind == "Kick" ) ImGui.TextWrapped( "Kick intent; confirm its outcome in activity." );
	}

	private void DrawActivity()
	{
		ImGui.TextUnformatted( "Recent activity visible to you" );
		if ( Model.Activity is null ) { ImGui.TextUnformatted( "Loading..." ); return; }
		if ( Model.Activity.Status != CommandStatus.Success ) ImGui.TextWrapped( "{0}", Model.Activity.Message );
		foreach ( var row in Model.Activity.Activity )
		{
			ImGui.TextWrapped( "{0:u}  {1}  {2}: {3}", row.CreatedUtc, row.Actor, row.Command, row.Outcome );
			ImGui.Text( "Request: {0:N}", row.RequestId );
		}
	}
}