Вернуть команду adminwho в общий доступ и реализовать UI с правильным client-server взаимодействием (#2918)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: VigersRay <60344369+VigersRay@users.noreply.github.com>
Co-authored-by: Vigers Ray <vigersray@gmail.com>
This commit is contained in:
Copilot 2025-09-09 22:06:57 +03:00 committed by GitHub
parent 5d085ad9c8
commit 878167c937
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 388 additions and 5 deletions

View file

@ -0,0 +1,31 @@
using Content.Shared.Administration;
namespace Content.Client.Administration.Systems;
/// <summary>
/// Client system for handling admin who requests
/// </summary>
public sealed class AdminWhoSystem : EntitySystem
{
public event Action<List<AdminWhoEntry>>? OnAdminWhoUpdate;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<AdminWhoResponseEvent>(OnAdminWhoResponse);
}
/// <summary>
/// Request the list of online administrators from the server
/// </summary>
public void RequestAdminWho()
{
RaiseNetworkEvent(new RequestAdminWhoEvent());
}
private void OnAdminWhoResponse(AdminWhoResponseEvent args, EntitySessionEventArgs session)
{
OnAdminWhoUpdate?.Invoke(args.Admins);
}
}

View file

@ -0,0 +1,65 @@
using Content.Client.Administration.Systems;
using JetBrains.Annotations;
using Robust.Client.UserInterface.Controllers;
namespace Content.Client.Administration.UI.Bwoink
{
[UsedImplicitly]
public sealed class AdminWhoUIController : UIController, IOnSystemChanged<AdminWhoSystem>
{
private AdminWhoWindow? _dialog;
private AdminWhoSystem? _adminWhoSystem;
protected override string SawmillName => "c.c.admin.adminwho";
public void OnSystemLoaded(AdminWhoSystem system)
{
_adminWhoSystem = system;
_dialog = new AdminWhoWindow();
_dialog.Initialize(system);
}
public void OnSystemUnloaded(AdminWhoSystem system)
{
if (_dialog != null)
{
_dialog.Close();
_dialog.Uninitialize();
_dialog.Dispose();
_dialog = null;
}
_adminWhoSystem = null;
}
public void Open()
{
if (_dialog == null)
{
if (_adminWhoSystem == null)
return;
_dialog = new AdminWhoWindow();
_dialog.Initialize(_adminWhoSystem);
}
_dialog.OpenCentered();
_dialog.RefreshAdminList();
}
public void Toggle()
{
if (_dialog?.IsOpen == true)
Close();
else
Open();
}
public void Close()
{
_dialog?.Close();
}
}
}

View file

@ -0,0 +1,23 @@
<ui:FancyWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
VerticalExpand="True" HorizontalExpand="True"
Title="{Loc admin-who-title}"
SetSize="400 300"
Resizable="False">
<PanelContainer StyleClasses="BackgroundDark">
<BoxContainer Orientation="Vertical" Margin="8">
<Label Name="LoadingLabel" Access="Public" Text="{Loc admin-who-loading}" StyleClasses="LabelText"
HorizontalAlignment="Center" Visible="False" />
<Label Name="NoAdminsLabel" Access="Public" Text="{Loc admin-who-no-admins}" StyleClasses="LabelText"
HorizontalAlignment="Center" Visible="False" />
<ScrollContainer VerticalExpand="True" HorizontalExpand="True" HScrollEnabled="False">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<BoxContainer Orientation="Vertical" Name="AdminsContainer" Access="Public" VerticalExpand="True" />
</BoxContainer>
</ScrollContainer>
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 8 0 0">
<Button Name="RefreshButton" Access="Public" Text="{Loc admin-who-refresh}" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</ui:FancyWindow>

View file

@ -0,0 +1,102 @@
using System.Text;
using Content.Client.Administration.Managers;
using Content.Client.Administration.Systems;
using Content.Client.UserInterface.Controls;
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.Bwoink
{
[GenerateTypedNameReferences]
public sealed partial class AdminWhoWindow : FancyWindow
{
private AdminWhoSystem? _adminWhoSystem;
public AdminWhoWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
RefreshButton.OnPressed += _ => RefreshAdminList();
CloseButton.OnPressed += _ => Close();
}
public void Initialize(AdminWhoSystem system)
{
_adminWhoSystem = system;
_adminWhoSystem.OnAdminWhoUpdate += OnAdminListReceived;
// Request the list when the window is initialized
RefreshAdminList();
}
public void Uninitialize()
{
if (_adminWhoSystem != null)
{
_adminWhoSystem.OnAdminWhoUpdate -= OnAdminListReceived;
_adminWhoSystem = null;
}
}
public void RefreshAdminList()
{
AdminsContainer.RemoveAllChildren();
NoAdminsLabel.Visible = false;
LoadingLabel.Visible = true;
_adminWhoSystem?.RequestAdminWho();
}
private void OnAdminListReceived(List<AdminWhoEntry> admins)
{
LoadingLabel.Visible = false;
if (admins.Count == 0)
{
NoAdminsLabel.Visible = true;
return;
}
NoAdminsLabel.Visible = false;
// Add each admin
foreach (var admin in admins)
{
var adminText = new StringBuilder();
adminText.Append(admin.Name);
if (!string.IsNullOrEmpty(admin.Title))
adminText.Append($": [{admin.Title}]");
if (admin.IsStealth)
adminText.Append(" (S)");
if (admin.IsAfk)
adminText.Append(" [AFK]");
var adminLabel = new Label
{
Text = adminText.ToString(),
StyleClasses = { "LabelText" },
HorizontalAlignment = Control.HAlignment.Left,
Margin = new Thickness(16, 2, 8, 2)
};
AdminsContainer.AddChild(adminLabel);
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Uninitialize();
}
base.Dispose(disposing);
}
}
}

View file

@ -16,6 +16,8 @@
<CheckBox Name="PlaySound" Access="Public" Text="{Loc 'admin-bwoink-play-sound'}" Pressed="True" />
<Control HorizontalExpand="True" MinWidth="5" />
<Button Visible="True" Name="PopOut" Access="Public" Text="{Loc 'admin-logs-pop-out'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
<Control HorizontalExpand="True" MinWidth="5" />
<Button Visible="True" Name="AdminWho" Access="Public" Text="{Loc 'admin-who-button'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
<Control HorizontalExpand="True" />
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" />
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />

View file

@ -203,6 +203,14 @@ namespace Content.Client.Administration.UI.Bwoink
{
uiController.PopOut();
};
// Sunrise-Start
AdminWho.OnPressed += _ =>
{
var ctrl = _ui.GetUIController<AdminWhoUIController>();
ctrl.Toggle();
};
// Sunrise-End
}
public void OnBwoink(NetUserId channel)

View file

@ -8,4 +8,9 @@
<RichTextLabel Name="TypingIndicator" Access="Public" />
<HistoryLineEdit Name="SenderLineEdit" />
<RichTextLabel Name="RelayedToDiscordLabel" Access="Public" Visible="False" />
<!-- Sunrise-Start -->
<BoxContainer Orientation="Horizontal" Margin="0 4 0 0">
<Button Name="AdminWhoButton" Access="Public" Text="{Loc 'admin-who-button'}" Visible="False" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Sunrise-End -->
</BoxContainer>

View file

@ -1,5 +1,6 @@
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
@ -10,6 +11,8 @@ namespace Content.Client.Administration.UI.Bwoink
[GenerateTypedNameReferences]
public sealed partial class BwoinkPanel : BoxContainer
{
[Dependency] private readonly IUserInterfaceManager _ui = default!;
private readonly Action<string> _messageSender;
public int Unread { get; private set; } = 0;
@ -25,6 +28,7 @@ namespace Content.Client.Administration.UI.Bwoink
public BwoinkPanel(Action<string> messageSender)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this); // Sunrise-Edit
var msg = new FormattedMessage();
msg.PushColor(Color.LightGray);
@ -41,6 +45,15 @@ namespace Content.Client.Administration.UI.Bwoink
};
SenderLineEdit.OnTextEntered += Input_OnTextEntered;
SenderLineEdit.OnTextChanged += Input_OnTextChanged;
// Sunrise-Start
AdminWhoButton.OnPressed += _ =>
{
var ctrl = _ui.GetUIController<AdminWhoUIController>();
ctrl.Toggle();
};
// Sunrise-End
UpdateTypingIndicator();
}

View file

@ -637,7 +637,10 @@ public sealed class UserAHelpUIHandler : IAHelpUIHandler
if (_window is { Disposed: false })
return;
_chatPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(_ownerId, text, true, false));
_chatPanel.AHelpDescLabel.Visible = true; // Sunrise-Edit
// Sunrise-Start
_chatPanel.AHelpDescLabel.Visible = true;
_chatPanel.AdminWhoButton.Visible = true;
// Sunrise-End
_chatPanel.InputTextChanged += text => InputTextChanged?.Invoke(_ownerId, text);
_chatPanel.RelayedToDiscordLabel.Visible = relayActive;
_window = new DefaultWindow()

View file

@ -5,13 +5,13 @@
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Horizontal" SetHeight="40" HorizontalExpand="True" Margin="5">
<Button Name="NewTicketButton" Access="Public" Text="{Loc 'mentor-help-new-ticket'}"
StyleClasses="ButtonBig" />
<Control SetWidth="10" />
StyleClasses="ButtonBig" Margin="0 0 10 0"/>
<Button Name="AdminWhoButton" Access="Public" Text="{Loc 'admin-who-button'}" Margin="0 0 10 0" />
<Button Name="StatisticsButton" Access="Public" Text="{Loc 'mentor-help-statistics'}"
StyleClasses="ButtonBig" Visible="False" />
StyleClasses="ButtonBig" Visible="False" Margin="0 0 10 0" />
<Control HorizontalExpand="True" />
<Button Name="BackToListButton" Access="Public" Text="{Loc 'mentor-help-back-to-list'}"
StyleClasses="ButtonBig" Visible="False" />
StyleClasses="ButtonBig" HorizontalAlignment="Right" Visible="False" />
</BoxContainer>
<Control Name="DefaultState" VerticalExpand="True" HorizontalExpand="True">

View file

@ -1,9 +1,11 @@
using System.Linq;
using Content.Client.Administration.Systems;
using Content.Client.Administration.UI.Bwoink;
using Content.Shared._Sunrise.MentorHelp;
using Content.Shared.Administration;
using Content.Shared.Database;
using Robust.Client.AutoGenerated;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@ -65,6 +67,12 @@ namespace Content.Client._Sunrise.MentorHelp
// Handle tab changes to load closed tickets when needed
TicketsTabContainer.OnTabChanged += OnTabChanged;
AdminWhoButton.OnPressed += _ =>
{
var ctrl = _ui.GetUIController<AdminWhoUIController>();
ctrl.Toggle();
};
// Set initial state
SwitchState(ViewState.TicketsList);
}

View file

@ -0,0 +1,64 @@
using Content.Server.Administration.Managers;
using Content.Server.Afk;
using Content.Shared.Administration;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Systems;
/// <summary>
/// Server system for handling admin who requests
/// </summary>
public sealed class AdminWhoSystem : EntitySystem
{
[Dependency] private readonly IAfkManager _afkManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<RequestAdminWhoEvent>(OnRequestAdminWho);
}
private void OnRequestAdminWho(RequestAdminWhoEvent args, EntitySessionEventArgs session)
{
if (session.SenderSession == null)
return;
var seeStealth = false;
var seeAfk = false;
// Check if the requesting player can see stealth admins and AFK status
if (session.SenderSession.AttachedEntity != null)
{
var playerData = _adminManager.GetAdminData(session.SenderSession);
if (playerData != null)
{
seeStealth = playerData.CanStealth();
seeAfk = _adminManager.HasAdminFlag(session.SenderSession, AdminFlags.Admin);
}
}
var adminList = new List<AdminWhoEntry>();
foreach (var admin in _adminManager.ActiveAdmins)
{
var adminData = _adminManager.GetAdminData(admin);
DebugTools.AssertNotNull(adminData);
if (adminData!.Stealth && !seeStealth)
continue;
var isAfk = seeAfk && _afkManager.IsAfk(admin);
adminList.Add(new AdminWhoEntry(
admin.Name,
adminData.Title,
adminData.Stealth,
isAfk
));
}
RaiseNetworkEvent(new AdminWhoResponseEvent(adminList), session.SenderSession);
}
}

View file

@ -0,0 +1,45 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Administration;
/// <summary>
/// Request event to get the list of online administrators
/// </summary>
[Serializable, NetSerializable]
public sealed class RequestAdminWhoEvent : EntityEventArgs
{
}
/// <summary>
/// Response event containing the list of online administrators
/// </summary>
[Serializable, NetSerializable]
public sealed class AdminWhoResponseEvent : EntityEventArgs
{
public readonly List<AdminWhoEntry> Admins;
public AdminWhoResponseEvent(List<AdminWhoEntry> admins)
{
Admins = admins;
}
}
/// <summary>
/// Information about a single administrator
/// </summary>
[Serializable, NetSerializable]
public sealed class AdminWhoEntry
{
public readonly string Name;
public readonly string? Title;
public readonly bool IsStealth;
public readonly bool IsAfk;
public AdminWhoEntry(string name, string? title, bool isStealth, bool isAfk)
{
Name = name;
Title = title;
IsStealth = isStealth;
IsAfk = isAfk;
}
}

View file

@ -0,0 +1,7 @@
admin-who-button = Admins Online
admin-who-title = Online Administrators
admin-who-refresh = Refresh
admin-who-close = Close
admin-who-no-admins = No administrators are currently online.
admin-who-loading = Loading administrators...
admin-who-info = This feature displays which administrators are currently online and available to help with any issues you may have.

View file

@ -0,0 +1,7 @@
admin-who-button = Админы онлайн
admin-who-title = Администраторы онлайн
admin-who-refresh = Обновить
admin-who-close = Закрыть
admin-who-no-admins = В данный момент нет администраторов онлайн.
admin-who-loading = Загрузка администраторов...
admin-who-info = Эта функция показывает, какие администраторы в данный момент онлайн и доступны для помощи с любыми вопросами.