From: DrSmugleaf Date: Thu, 12 Oct 2023 22:45:04 +0000 (-0700) Subject: Add support for multiple changelog files, add admin changelog (#20849) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=d6575eb556ecf407964f37194916ec7540ad2e9d;p=space-station-14.git Add support for multiple changelog files, add admin changelog (#20849) --- diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs index 66c8b8a063..8978e2fd6d 100644 --- a/Content.Client/Administration/Managers/ClientAdminManager.cs +++ b/Content.Client/Administration/Managers/ClientAdminManager.cs @@ -130,5 +130,13 @@ namespace Content.Client.Administration.Managers 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 46e3a01537..b4b5b48b81 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 @@ namespace Content.Client.Administration.Managers /// 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 @@ namespace Content.Client.Administration.Managers 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/Changelog/ChangelogManager.cs b/Content.Client/Changelog/ChangelogManager.cs index 249332c337..396af99d2c 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 @@ namespace Content.Client.Changelog 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; + } - if (changelog.Count == 0) + var changelog = changelogs[0]; + if (mainChangelogs.Length > 1) + { + _sawmill.Error($"More than one file found in Resource/Changelog with name {MainChangelogName}"); + } + + 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,20 +93,74 @@ namespace Content.Client.Changelog NewChangelogEntriesChanged?.Invoke(); } - public Task> LoadChangelog() + public Task> LoadChangelog() { return Task.Run(() => { - var yamlData = _resource.ContentFileReadYaml(new ("/Changelog/Changelog.yml")); + 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) - return new List(); + if (yamlData.Documents.Count == 0) + continue; - var node = (MappingDataNode)yamlData.Documents[0].RootNode.ToDataNode(); - return _serialization.Read>(node["Entries"], notNullableOverride: true); + var node = yamlData.Documents[0].RootNode.ToDataNodeCast(); + var changelog = _serialization.Read(node, notNullableOverride: true); + if (string.IsNullOrWhiteSpace(changelog.Name)) + changelog.Name = file.FilenameWithoutExtension; + + 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 { @@ -108,7 +184,7 @@ namespace Content.Client.Changelog } [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 0000000000..7c049efacc --- /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 0000000000..d1e2bc7533 --- /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 888a8528d9..355452dbfa 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 cea5bd9e7c..e5f492900c 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 @@ namespace Content.Client.Changelog 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/Stylesheets/StyleSpace.cs b/Content.Client/Stylesheets/StyleSpace.cs index a82dba65bc..3bb4e986af 100644 --- a/Content.Client/Stylesheets/StyleSpace.cs +++ b/Content.Client/Stylesheets/StyleSpace.cs @@ -4,7 +4,6 @@ using Robust.Client.Graphics; 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 @@ namespace Content.Client.Stylesheets 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