diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs index 66c8b8a0630..8978e2fd6dd 100644 --- a/Content.Client/Administration/Managers/ClientAdminManager.cs +++ b/Content.Client/Administration/Managers/ClientAdminManager.cs @@ -130,5 +130,13 @@ void IPostInjectInit.PostInject() return null; } + + public AdminData? GetAdminData(bool includeDeAdmin = false) + { + if (_player.LocalPlayer is { Session: { } session }) + return GetAdminData(session, includeDeAdmin); + + return null; + } } } diff --git a/Content.Client/Administration/Managers/IClientAdminManager.cs b/Content.Client/Administration/Managers/IClientAdminManager.cs index 46e3a01537b..b4b5b48b814 100644 --- a/Content.Client/Administration/Managers/IClientAdminManager.cs +++ b/Content.Client/Administration/Managers/IClientAdminManager.cs @@ -1,5 +1,4 @@ -using System; -using Content.Shared.Administration; +using Content.Shared.Administration; namespace Content.Client.Administration.Managers { @@ -13,6 +12,15 @@ public interface IClientAdminManager /// event Action AdminStatusUpdated; + /// + /// Gets the admin data for the client, if they are an admin. + /// + /// + /// Whether to return admin data for admins that are current de-adminned. + /// + /// if the player is not an admin. + AdminData? GetAdminData(bool includeDeAdmin = false); + /// /// Checks whether the local player is an admin. /// @@ -52,5 +60,17 @@ public interface IClientAdminManager bool CanAdminMenu(); void Initialize(); + + /// + /// Checks if the client is an admin. + /// + /// + /// Whether to return admin data for admins that are current de-adminned. + /// + /// true if the player is an admin, false otherwise. + bool IsAdmin(bool includeDeAdmin = false) + { + return GetAdminData(includeDeAdmin) != null; + } } } diff --git a/Content.Client/Administration/UI/AdminMenuWindow.xaml b/Content.Client/Administration/UI/AdminMenuWindow.xaml index 49eb9c0de60..311d67b826c 100644 --- a/Content.Client/Administration/UI/AdminMenuWindow.xaml +++ b/Content.Client/Administration/UI/AdminMenuWindow.xaml @@ -5,13 +5,15 @@ xmlns:atmosTab="clr-namespace:Content.Client.Administration.UI.Tabs.AtmosTab" xmlns:tabs="clr-namespace:Content.Client.Administration.UI.Tabs" xmlns:playerTab="clr-namespace:Content.Client.Administration.UI.Tabs.PlayerTab" - xmlns:objectsTab="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab"> + xmlns:objectsTab="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab" + xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab"> + diff --git a/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs b/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs index c15e56147dd..c3ea67a3edb 100644 --- a/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs +++ b/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs @@ -12,7 +12,7 @@ public sealed partial class AdminMenuWindow : DefaultWindow public AdminMenuWindow() { - MinSize = new Vector2(600, 250); // Corvax-Resize + MinSize = new Vector2(650, 250); Title = Loc.GetString("admin-menu-title"); RobustXamlLoader.Load(this); MasterTabContainer.SetTabTitle(0, Loc.GetString("admin-menu-admin-tab")); @@ -20,8 +20,9 @@ public AdminMenuWindow() MasterTabContainer.SetTabTitle(2, Loc.GetString("admin-menu-atmos-tab")); MasterTabContainer.SetTabTitle(3, Loc.GetString("admin-menu-round-tab")); MasterTabContainer.SetTabTitle(4, Loc.GetString("admin-menu-server-tab")); - MasterTabContainer.SetTabTitle(5, Loc.GetString("admin-menu-players-tab")); - MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-objects-tab")); + MasterTabContainer.SetTabTitle(5, Loc.GetString("admin-menu-panic-bunker-tab")); + MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-players-tab")); + MasterTabContainer.SetTabTitle(7, Loc.GetString("admin-menu-objects-tab")); } protected override void Dispose(bool disposing) diff --git a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerStatusWindow.xaml b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerStatusWindow.xaml new file mode 100644 index 00000000000..633bef05148 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerStatusWindow.xaml @@ -0,0 +1,6 @@ + + diff --git a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerStatusWindow.xaml.cs b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerStatusWindow.xaml.cs new file mode 100644 index 00000000000..ec16bf6aea7 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerStatusWindow.xaml.cs @@ -0,0 +1,14 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Administration.UI.Tabs.PanicBunkerTab; + +[GenerateTypedNameReferences] +public sealed partial class PanicBunkerStatusWindow : DefaultWindow +{ + public PanicBunkerStatusWindow() + { + RobustXamlLoader.Load(this); + } +} diff --git a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml new file mode 100644 index 00000000000..89827d06424 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs new file mode 100644 index 00000000000..e9d3b95c5d8 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs @@ -0,0 +1,54 @@ +using Content.Shared.Administration.Events; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Console; + +namespace Content.Client.Administration.UI.Tabs.PanicBunkerTab; + +[GenerateTypedNameReferences] +public sealed partial class PanicBunkerTab : Control +{ + [Dependency] private readonly IConsoleHost _console = default!; + + public PanicBunkerTab() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + DisableAutomaticallyButton.ToolTip = Loc.GetString("admin-ui-panic-bunker-disable-automatically-tooltip"); + + MinAccountAge.OnTextEntered += args => + { + if (string.IsNullOrWhiteSpace(args.Text) || !int.TryParse(args.Text, out var minutes)) + return; + + _console.ExecuteCommand($"panicbunker_min_account_age {minutes}"); + }; + + MinOverallHours.OnTextEntered += args => + { + if (string.IsNullOrWhiteSpace(args.Text) || !int.TryParse(args.Text, out var hours)) + return; + + _console.ExecuteCommand($"panicbunker_min_overall_hours {hours}"); + }; + } + + public void UpdateStatus(PanicBunkerStatus status) + { + EnabledButton.Pressed = status.Enabled; + EnabledButton.Text = Loc.GetString(status.Enabled + ? "admin-ui-panic-bunker-enabled" + : "admin-ui-panic-bunker-disabled" + ); + EnabledButton.ModulateSelfOverride = status.Enabled ? Color.Red : null; + + DisableAutomaticallyButton.Pressed = status.DisableWithAdmins; + EnableAutomaticallyButton.Pressed = status.EnableWithoutAdmins; + CountDeadminnedButton.Pressed = status.CountDeadminnedAdmins; + ShowReasonButton.Pressed = status.ShowReason; + MinAccountAge.Text = status.MinAccountAgeHours.ToString(); + MinOverallHours.Text = status.MinOverallHours.ToString(); + } +} diff --git a/Content.Client/Administration/UI/Tabs/ServerTab.xaml b/Content.Client/Administration/UI/Tabs/ServerTab.xaml index 7e15bc27539..b9984058358 100644 --- a/Content.Client/Administration/UI/Tabs/ServerTab.xaml +++ b/Content.Client/Administration/UI/Tabs/ServerTab.xaml @@ -8,6 +8,5 @@ - diff --git a/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs index b83a3d1ec03..24b92e42ce7 100644 --- a/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs @@ -18,7 +18,6 @@ public ServerTab() _config.OnValueChanged(CCVars.OocEnabled, OocEnabledChanged, true); _config.OnValueChanged(CCVars.LoocEnabled, LoocEnabledChanged, true); - _config.OnValueChanged(CCVars.PanicBunkerEnabled, BunkerEnabledChanged, true); } private void OocEnabledChanged(bool value) @@ -31,11 +30,6 @@ private void LoocEnabledChanged(bool value) SetLoocButton.Pressed = value; } - private void BunkerEnabledChanged(bool value) - { - SetPanicbunkerButton.Pressed = value; - } - protected override void Dispose(bool disposing) { base.Dispose(disposing); @@ -44,7 +38,6 @@ protected override void Dispose(bool disposing) { _config.UnsubValueChanged(CCVars.OocEnabled, OocEnabledChanged); _config.UnsubValueChanged(CCVars.LoocEnabled, LoocEnabledChanged); - _config.UnsubValueChanged(CCVars.PanicBunkerEnabled, BunkerEnabledChanged); } } } diff --git a/Content.Client/Changelog/ChangelogManager.cs b/Content.Client/Changelog/ChangelogManager.cs index 40a8435e6ea..396af99d2cf 100644 --- a/Content.Client/Changelog/ChangelogManager.cs +++ b/Content.Client/Changelog/ChangelogManager.cs @@ -1,29 +1,29 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Threading.Tasks; using Content.Shared.CCVar; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; -using Robust.Shared.IoC; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager; -using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Markdown; using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Utility; - namespace Content.Client.Changelog { - public sealed partial class ChangelogManager + public sealed partial class ChangelogManager : IPostInjectInit { + [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IResourceManager _resource = default!; [Dependency] private readonly ISerializationManager _serialization = default!; [Dependency] private readonly IConfigurationManager _configManager = default!; + private const string SawmillName = "changelog"; + public const string MainChangelogName = "Changelog"; + + private ISawmill _sawmill = default!; + public bool NewChangelogEntries { get; private set; } public int LastReadId { get; private set; } public int MaxId { get; private set; } @@ -51,17 +51,39 @@ public void SaveNewReadId() public async void Initialize() { // Open changelog purely to compare to the last viewed date. - var changelog = await LoadChangelog(); + var changelogs = await LoadChangelog(); + UpdateChangelogs(changelogs); + } + + private void UpdateChangelogs(List changelogs) + { + if (changelogs.Count == 0) + { + return; + } + + var mainChangelogs = changelogs.Where(c => c.Name == MainChangelogName).ToArray(); + if (mainChangelogs.Length == 0) + { + _sawmill.Error($"No changelog file found in Resources/Changelog with name {MainChangelogName}"); + return; + } + + var changelog = changelogs[0]; + if (mainChangelogs.Length > 1) + { + _sawmill.Error($"More than one file found in Resource/Changelog with name {MainChangelogName}"); + } - if (changelog.Count == 0) + if (changelog.Entries.Count == 0) { return; } - MaxId = changelog.Max(c => c.Id); + MaxId = changelog.Entries.Max(c => c.Id); var path = new ResPath($"/changelog_last_seen_{_configManager.GetCVar(CCVars.ServerId)}"); - if(_resource.UserData.TryReadAllText(path, out var lastReadIdText)) + if (_resource.UserData.TryReadAllText(path, out var lastReadIdText)) { LastReadId = int.Parse(lastReadIdText); } @@ -71,37 +93,74 @@ public async void Initialize() NewChangelogEntriesChanged?.Invoke(); } - // Corvax-MultiChangelog-Start - public async Task> LoadChangelog() - { - var paths = _resource.ContentFindFiles("/Changelog/") - .Where(filePath => filePath.Extension == "yml") - .ToArray(); - - var result = new List(); - foreach (var path in paths) - { - var changelog = await LoadChangelogFile(path); - result = result.Union(changelog).ToList(); - } - return result.OrderBy(x => x.Time).ToList(); - } - // Corvax-MultiChangelog-End - - private Task> LoadChangelogFile(ResPath path) + public Task> LoadChangelog() { return Task.Run(() => { - var yamlData = _resource.ContentFileReadYaml(path); + var changelogs = new List(); + var directory = new ResPath("/Changelog"); + foreach (var file in _resource.ContentFindFiles(new ResPath("/Changelog/"))) + { + if (file.Directory != directory || file.Extension != "yml") + continue; + + var yamlData = _resource.ContentFileReadYaml(file); + + if (yamlData.Documents.Count == 0) + continue; - if (yamlData.Documents.Count == 0) - return new List(); + var node = yamlData.Documents[0].RootNode.ToDataNodeCast(); + var changelog = _serialization.Read(node, notNullableOverride: true); + if (string.IsNullOrWhiteSpace(changelog.Name)) + changelog.Name = file.FilenameWithoutExtension; - var node = (MappingDataNode)yamlData.Documents[0].RootNode.ToDataNode(); - return _serialization.Read>(node["Entries"], notNullableOverride: true); + changelogs.Add(changelog); + } + + changelogs.Sort((a, b) => a.Order.CompareTo(b.Order)); + return changelogs; }); } + public void PostInject() + { + _sawmill = _logManager.GetSawmill(SawmillName); + } + + [DataDefinition] + public sealed partial class Changelog + { + /// + /// The name to use for this changelog. + /// If left unspecified, the name of the file is used instead. + /// Used during localization to find the user-displayed name of this changelog. + /// + [DataField("Name")] + public string Name = string.Empty; + + /// + /// The individual entries in this changelog. + /// These are not kept around in memory in the changelog manager. + /// + [DataField("Entries")] + public List Entries = new(); + + /// + /// Whether or not this changelog will be displayed as a tab to non-admins. + /// These are still loaded by all clients, but not shown if they aren't an admin, + /// as they do not contain sensitive data and are publicly visible on GitHub. + /// + [DataField("AdminOnly")] + public bool AdminOnly; + + /// + /// Used when ordering the changelog tabs for the user to see. + /// Larger numbers are displayed later, smaller numbers are displayed earlier. + /// + [DataField("Order")] + public int Order; + } + [DataDefinition] public sealed partial class ChangelogEntry : ISerializationHooks { @@ -125,7 +184,7 @@ void ISerializationHooks.AfterDeserialization() } [DataDefinition] - public sealed partial class ChangelogChange : ISerializationHooks + public sealed partial class ChangelogChange { [DataField("type")] public ChangelogLineType Type { get; private set; } diff --git a/Content.Client/Changelog/ChangelogTab.xaml b/Content.Client/Changelog/ChangelogTab.xaml new file mode 100644 index 00000000000..7c049efacc7 --- /dev/null +++ b/Content.Client/Changelog/ChangelogTab.xaml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/Content.Client/Changelog/ChangelogTab.xaml.cs b/Content.Client/Changelog/ChangelogTab.xaml.cs new file mode 100644 index 00000000000..d1e2bc7533e --- /dev/null +++ b/Content.Client/Changelog/ChangelogTab.xaml.cs @@ -0,0 +1,175 @@ +using System.Linq; +using System.Numerics; +using Content.Client.Resources; +using Content.Client.Stylesheets; +using Robust.Client.AutoGenerated; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Utility; +using static Content.Client.Changelog.ChangelogManager; +using static Robust.Client.UserInterface.Controls.BoxContainer; + +namespace Content.Client.Changelog; + +[GenerateTypedNameReferences] +public sealed partial class ChangelogTab : Control +{ + [Dependency] private readonly ChangelogManager _changelog = default!; + [Dependency] private readonly IResourceCache _resourceCache = default!; + + public bool AdminOnly; + + public ChangelogTab() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + } + + public void PopulateChangelog(ChangelogManager.Changelog changelog) + { + var byDay = changelog.Entries + .GroupBy(e => e.Time.ToLocalTime().Date) + .OrderByDescending(c => c.Key); + + var hasRead = changelog.Name != MainChangelogName || + _changelog.MaxId <= _changelog.LastReadId; + + foreach (var dayEntries in byDay) + { + var day = dayEntries.Key; + + var groupedEntries = dayEntries + .GroupBy(c => (c.Author, Read: c.Id <= _changelog.LastReadId)) + .OrderBy(c => c.Key.Read) + .ThenBy(c => c.Key.Author); + + string dayNice; + var today = DateTime.Today; + if (day == today) + dayNice = Loc.GetString("changelog-today"); + else if (day == today.AddDays(-1)) + dayNice = Loc.GetString("changelog-yesterday"); + else + dayNice = day.ToShortDateString(); + + ChangelogBody.AddChild(new Label + { + Text = dayNice, + StyleClasses = { StyleBase.StyleClassLabelHeading }, + Margin = new Thickness(4, 6, 0, 0) + }); + + var first = true; + + foreach (var groupedEntry in groupedEntries) + { + var (author, read) = groupedEntry.Key; + + if (!first) + { + ChangelogBody.AddChild(new Control { Margin = new Thickness(4) }); + } + + if (read && !hasRead) + { + hasRead = true; + + var upArrow = + _resourceCache.GetTexture("/Textures/Interface/Changelog/up_arrow.svg.192dpi.png"); + + var readDivider = new BoxContainer + { + Orientation = LayoutOrientation.Vertical + }; + + var hBox = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalAlignment = HAlignment.Center, + Children = + { + new TextureRect + { + Texture = upArrow, + ModulateSelfOverride = Color.FromHex("#888"), + TextureScale = new Vector2(0.5f, 0.5f), + Margin = new Thickness(4, 3), + VerticalAlignment = VAlignment.Bottom + }, + new Label + { + Align = Label.AlignMode.Center, + Text = Loc.GetString("changelog-new-changes"), + FontColorOverride = Color.FromHex("#888"), + }, + new TextureRect + { + Texture = upArrow, + ModulateSelfOverride = Color.FromHex("#888"), + TextureScale = new Vector2(0.5f, 0.5f), + Margin = new Thickness(4, 3), + VerticalAlignment = VAlignment.Bottom + } + } + }; + + readDivider.AddChild(hBox); + readDivider.AddChild(new PanelContainer { StyleClasses = { StyleBase.ClassLowDivider } }); + ChangelogBody.AddChild(readDivider); + + if (first) + readDivider.SetPositionInParent(ChangelogBody.ChildCount - 2); + } + + first = false; + + var authorLabel = new RichTextLabel + { + Margin = new Thickness(6, 0, 0, 0), + }; + authorLabel.SetMessage( + FormattedMessage.FromMarkup(Loc.GetString("changelog-author-changed", ("author", author)))); + ChangelogBody.AddChild(authorLabel); + + foreach (var change in groupedEntry.SelectMany(c => c.Changes)) + { + var text = new RichTextLabel(); + text.SetMessage(FormattedMessage.FromMarkup(change.Message)); + ChangelogBody.AddChild(new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + Margin = new Thickness(14, 1, 10, 2), + Children = + { + GetIcon(change.Type), + text + } + }); + } + } + } + } + + private TextureRect GetIcon(ChangelogLineType type) + { + var (file, color) = type switch + { + ChangelogLineType.Add => ("plus.svg.192dpi.png", "#6ED18D"), + ChangelogLineType.Remove => ("minus.svg.192dpi.png", "#D16E6E"), + ChangelogLineType.Fix => ("bug.svg.192dpi.png", "#D1BA6E"), + ChangelogLineType.Tweak => ("wrench.svg.192dpi.png", "#6E96D1"), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + + return new TextureRect + { + Texture = _resourceCache.GetTexture(new ResPath($"/Textures/Interface/Changelog/{file}")), + VerticalAlignment = VAlignment.Top, + TextureScale = new Vector2(0.5f, 0.5f), + Margin = new Thickness(2, 4, 6, 2), + ModulateSelfOverride = Color.FromHex(color) + }; + } +} diff --git a/Content.Client/Changelog/ChangelogWindow.xaml b/Content.Client/Changelog/ChangelogWindow.xaml index 888a8528d91..355452dbfad 100644 --- a/Content.Client/Changelog/ChangelogWindow.xaml +++ b/Content.Client/Changelog/ChangelogWindow.xaml @@ -3,15 +3,10 @@ Title="{Loc 'changelog-window-title'}" MinSize="500 400" SetSize="500 400"> - - - - - - + - diff --git a/Content.Client/Changelog/ChangelogWindow.xaml.cs b/Content.Client/Changelog/ChangelogWindow.xaml.cs index cea5bd9e7c2..e5f492900c2 100644 --- a/Content.Client/Changelog/ChangelogWindow.xaml.cs +++ b/Content.Client/Changelog/ChangelogWindow.xaml.cs @@ -1,28 +1,22 @@ using System.Linq; -using System.Numerics; -using Content.Client.Resources; +using Content.Client.Administration.Managers; using Content.Client.Stylesheets; using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Systems.EscapeMenu; using Content.Shared.Administration; using JetBrains.Annotations; using Robust.Client.AutoGenerated; -using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Console; -using Robust.Shared.Utility; -using static Content.Client.Changelog.ChangelogManager; -using static Robust.Client.UserInterface.Controls.BoxContainer; namespace Content.Client.Changelog { [GenerateTypedNameReferences] public sealed partial class ChangelogWindow : FancyWindow { + [Dependency] private readonly IClientAdminManager _adminManager = default!; [Dependency] private readonly ChangelogManager _changelog = default!; - [Dependency] private readonly IResourceCache _resourceCache = default!; public ChangelogWindow() { @@ -39,154 +33,84 @@ protected override void Opened() PopulateChangelog(); } + protected override void EnteredTree() + { + base.EnteredTree(); + _adminManager.AdminStatusUpdated += OnAdminStatusUpdated; + } + + protected override void ExitedTree() + { + base.ExitedTree(); + _adminManager.AdminStatusUpdated -= OnAdminStatusUpdated; + } + + private void OnAdminStatusUpdated() + { + TabsUpdated(); + } + private async void PopulateChangelog() { // Changelog is not kept in memory so load it again. - var changelog = await _changelog.LoadChangelog(); + var changelogs = await _changelog.LoadChangelog(); - var byDay = changelog - .GroupBy(e => e.Time.ToLocalTime().Date) - .OrderByDescending(c => c.Key); + Tabs.DisposeAllChildren(); - var hasRead = _changelog.MaxId <= _changelog.LastReadId; - foreach (var dayEntries in byDay) + var i = 0; + foreach (var changelog in changelogs) { - var day = dayEntries.Key; - - var groupedEntries = dayEntries - .GroupBy(c => (c.Author, Read: c.Id <= _changelog.LastReadId)) - .OrderBy(c => c.Key.Read) - .ThenBy(c => c.Key.Author); - - string dayNice; - var today = DateTime.Today; - if (day == today) - dayNice = Loc.GetString("changelog-today"); - else if (day == today.AddDays(-1)) - dayNice = Loc.GetString("changelog-yesterday"); - else - dayNice = day.ToShortDateString(); - - ChangelogBody.AddChild(new Label - { - Text = dayNice, - StyleClasses = { StyleBase.StyleClassLabelHeading }, - Margin = new Thickness(4, 6, 0, 0) - }); + var tab = new ChangelogTab { AdminOnly = changelog.AdminOnly }; + tab.PopulateChangelog(changelog); - var first = true; - - foreach (var groupedEntry in groupedEntries) - { - var (author, read) = groupedEntry.Key; - - if (!first) - { - ChangelogBody.AddChild(new Control { Margin = new Thickness(4) }); - } - - if (read && !hasRead) - { - hasRead = true; - - var upArrow = - _resourceCache.GetTexture("/Textures/Interface/Changelog/up_arrow.svg.192dpi.png"); - - var readDivider = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }; - - var hBox = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - HorizontalAlignment = HAlignment.Center, - Children = - { - new TextureRect - { - Texture = upArrow, - ModulateSelfOverride = Color.FromHex("#888"), - TextureScale = new Vector2(0.5f, 0.5f), - Margin = new Thickness(4, 3), - VerticalAlignment = VAlignment.Bottom - }, - new Label - { - Align = Label.AlignMode.Center, - Text = Loc.GetString("changelog-new-changes"), - FontColorOverride = Color.FromHex("#888"), - }, - new TextureRect - { - Texture = upArrow, - ModulateSelfOverride = Color.FromHex("#888"), - TextureScale = new Vector2(0.5f, 0.5f), - Margin = new Thickness(4, 3), - VerticalAlignment = VAlignment.Bottom - } - } - }; - - readDivider.AddChild(hBox); - readDivider.AddChild(new PanelContainer { StyleClasses = { StyleBase.ClassLowDivider } }); - ChangelogBody.AddChild(readDivider); - - if (first) - readDivider.SetPositionInParent(ChangelogBody.ChildCount - 2); - } - - first = false; - - var authorLabel = new RichTextLabel - { - Margin = new Thickness(6, 0, 0, 0), - }; - authorLabel.SetMessage( - FormattedMessage.FromMarkup(Loc.GetString("changelog-author-changed", ("author", author)))); - ChangelogBody.AddChild(authorLabel); - - foreach (var change in groupedEntry.SelectMany(c => c.Changes)) - { - var text = new RichTextLabel(); - text.SetMessage(FormattedMessage.FromMarkup(change.Message)); - ChangelogBody.AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Margin = new Thickness(14, 1, 10, 2), - Children = - { - GetIcon(change.Type), - text - } - }); - } - } + Tabs.AddChild(tab); + Tabs.SetTabTitle(i++, Loc.GetString($"changelog-tab-title-{changelog.Name}")); } var version = typeof(ChangelogWindow).Assembly.GetName().Version ?? new Version(1, 0); VersionLabel.Text = Loc.GetString("changelog-version-tag", ("version", version.ToString())); + + TabsUpdated(); } - private TextureRect GetIcon(ChangelogLineType type) + private void TabsUpdated() { - var (file, color) = type switch + var tabs = Tabs.Children.OfType().ToArray(); + var isAdmin = _adminManager.IsAdmin(true); + + var visibleTabs = 0; + int? firstVisible = null; + for (var i = 0; i < tabs.Length; i++) { - ChangelogLineType.Add => ("plus.svg.192dpi.png", "#6ED18D"), - ChangelogLineType.Remove => ("minus.svg.192dpi.png", "#D16E6E"), - ChangelogLineType.Fix => ("bug.svg.192dpi.png", "#D1BA6E"), - ChangelogLineType.Tweak => ("wrench.svg.192dpi.png", "#6E96D1"), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; - - return new TextureRect + var tab = tabs[i]; + + if (!tab.AdminOnly || isAdmin) + { + Tabs.SetTabVisible(i, true); + tab.Visible = true; + visibleTabs++; + firstVisible ??= i; + } + else + { + Tabs.SetTabVisible(i, false); + tab.Visible = false; + } + } + + Tabs.TabsVisible = visibleTabs > 1; + + // Current tab became invisible, select the first one that is visible + if (!Tabs.GetTabVisible(Tabs.CurrentTab) && firstVisible != null) { - Texture = _resourceCache.GetTexture(new ResPath($"/Textures/Interface/Changelog/{file}")), - VerticalAlignment = VAlignment.Top, - TextureScale = new Vector2(0.5f, 0.5f), - Margin = new Thickness(2, 4, 6, 2), - ModulateSelfOverride = Color.FromHex(color) - }; + Tabs.CurrentTab = firstVisible.Value; + } + + // We are only displaying one tab, hide its header + if (!Tabs.TabsVisible && firstVisible != null) + { + Tabs.SetTabVisible(firstVisible.Value, false); + } } } diff --git a/Content.Client/Jittering/JitteringSystem.cs b/Content.Client/Jittering/JitteringSystem.cs index 032eb3e18f2..41f20634ab5 100644 --- a/Content.Client/Jittering/JitteringSystem.cs +++ b/Content.Client/Jittering/JitteringSystem.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Immutable; using System.Numerics; using Content.Shared.Jittering; using Robust.Client.Animations; using Robust.Client.GameObjects; -using Robust.Shared.Animations; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Maths; using Robust.Shared.Random; namespace Content.Client.Jittering @@ -15,6 +9,7 @@ namespace Content.Client.Jittering public sealed class JitteringSystem : SharedJitteringSystem { [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!; private readonly float[] _sign = { -1, 1 }; private readonly string _jitterAnimationKey = "jittering"; @@ -35,13 +30,13 @@ private void OnStartup(EntityUid uid, JitteringComponent jittering, ComponentSta var animationPlayer = EntityManager.EnsureComponent(uid); - animationPlayer.Play(GetAnimation(jittering, sprite), _jitterAnimationKey); + _animationPlayer.Play(animationPlayer, GetAnimation(jittering, sprite), _jitterAnimationKey); } private void OnShutdown(EntityUid uid, JitteringComponent jittering, ComponentShutdown args) { if (EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? animationPlayer)) - animationPlayer.Stop(_jitterAnimationKey); + _animationPlayer.Stop(animationPlayer, _jitterAnimationKey); if (EntityManager.TryGetComponent(uid, out SpriteComponent? sprite)) sprite.Offset = Vector2.Zero; @@ -52,9 +47,9 @@ private void OnAnimationCompleted(EntityUid uid, JitteringComponent jittering, A if(args.Key != _jitterAnimationKey) return; - if(EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? animationPlayer) + if (EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? animationPlayer) && EntityManager.TryGetComponent(uid, out SpriteComponent? sprite)) - animationPlayer.Play(GetAnimation(jittering, sprite), _jitterAnimationKey); + _animationPlayer.Play(animationPlayer, GetAnimation(jittering, sprite), _jitterAnimationKey); } private Animation GetAnimation(JitteringComponent jittering, SpriteComponent sprite) @@ -77,8 +72,10 @@ private Animation GetAnimation(JitteringComponent jittering, SpriteComponent spr offset.Y *= -1; } - // Animation length shouldn't be too high so we will cap it at 2 seconds... - var length = Math.Min((1f/jittering.Frequency), 2f); + var length = 0f; + // avoid dividing by 0 so animations don't try to be infinitely long + if (jittering.Frequency > 0) + length = 1f / jittering.Frequency; jittering.LastJitter = offset; diff --git a/Content.Client/Stylesheets/StyleSpace.cs b/Content.Client/Stylesheets/StyleSpace.cs index a82dba65bcc..3bb4e986af5 100644 --- a/Content.Client/Stylesheets/StyleSpace.cs +++ b/Content.Client/Stylesheets/StyleSpace.cs @@ -4,7 +4,6 @@ using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; -using Robust.Shared.Maths; using static Robust.Client.UserInterface.StylesheetHelpers; namespace Content.Client.Stylesheets @@ -62,6 +61,14 @@ public StyleSpace(IResourceCache resCache) : base(resCache) var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png"); + var tabContainerPanel = new StyleBoxTexture(); + tabContainerPanel.SetPatchMargin(StyleBox.Margin.All, 2); + + var tabContainerBoxActive = new StyleBoxFlat {BackgroundColor = new Color(64, 64, 64)}; + tabContainerBoxActive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5); + var tabContainerBoxInactive = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 32)}; + tabContainerBoxInactive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5); + Stylesheet = new Stylesheet(BaseRules.Concat(new StyleRule[] { Element