Add ability to include "experimental" icon for labeled PRs (#39686)

* Initial commit

* Include correct license

* Review changes

* And some stuff I missed

* Make entries have a separate control

* Remove readonly
This commit is contained in:
SlamBamActionman 2026-07-25 18:35:19 +02:00 • committed by GitHub
parent b22d38f9c0
commit 335f13c22a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 130 additions and 37 deletions

View file

@ -0,0 +1,8 @@
<controls:ChangelogEntry
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.Changelog">
<BoxContainer Margin="14 1 10 2">
<TextureRect Margin="2 4 0 2" Name="Icon" VerticalAlignment="Top" TextureScale="0.5 0.5"></TextureRect>
<RichTextLabel Margin="6 0 0 0" Name="Text"></RichTextLabel>
</BoxContainer>
</controls:ChangelogEntry>

View file

@ -0,0 +1,66 @@
using System.Numerics;
using Content.Client.Resources;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
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;
namespace Content.Client.Changelog;
[GenerateTypedNameReferences]
public sealed partial class ChangelogEntry : Control
{
[Dependency] private IResourceCache _resourceCache = default!;
public ChangelogEntry()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
public void SetText(FormattedMessage text)
{
Text.SetMessage(text);
}
public void SetIcons(ChangelogLineType type, bool experimental)
{
var icon = GetIcon(type);
Icon.Texture = icon.Item1;
Icon.ModulateSelfOverride = icon.Item2;
if (experimental)
{
var expIcon = new TextureRect
{
Texture = _resourceCache.GetTexture(
new ResPath("/Textures/Interface/Changelog/test-tube.svg.192dpi.png")),
TextureScale = new Vector2(0.5f, 0.5f),
VerticalAlignment = VAlignment.Top,
Margin = new Thickness(2, 4, 0, 2),
ModulateSelfOverride = Color.FromHex("#A0d16E"),
};
Icon.Parent!.AddChild(expIcon);
expIcon.SetPositionInParent(1);
}
}
private (Texture, Color) 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 (_resourceCache.GetTexture(new ResPath($"/Textures/Interface/Changelog/{file}")), Color.FromHex(color));
}
}

View file

@ -195,17 +195,23 @@ namespace Content.Client.Changelog
[DataDefinition]
public sealed partial class ChangelogEntry
{
[DataField("id")]
[DataField]
public int Id { get; private set; }
[DataField("author")]
[DataField]
public string Author { get; private set; } = "";
[DataField]
public DateTime Time { get; private set; }
[DataField("changes")]
[DataField]
public List<ChangelogChange> Changes { get; private set; } = default!;
/// <summary>
/// Labels attached to the related PR, passed on via the SS14.Changelog parser.
/// </summary>
[DataField]
public List<string> Labels { get; private set; } = [];
}
[DataDefinition]

View file

@ -7,7 +7,6 @@ using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.ContentPack;
using Robust.Shared.Utility;
using static Content.Client.Changelog.ChangelogManager;
using static Robust.Client.UserInterface.Controls.BoxContainer;
@ -22,6 +21,11 @@ public sealed partial class ChangelogTab : Control
public bool AdminOnly;
/// <summary>
/// Changelog is from a PR marked "Experimental" and should therefore have an additional icon.
/// </summary>
private static string _experimentalString = "Intent: Experimental";
public ChangelogTab()
{
RobustXamlLoader.Load(this);
@ -134,43 +138,17 @@ public sealed partial class ChangelogTab : Control
FormattedMessage.FromMarkupOrThrow(Loc.GetString("changelog-author-changed", ("author", FormattedMessage.EscapeText(author)))));
ChangelogBody.AddChild(authorLabel);
foreach (var change in groupedEntry.SelectMany(c => c.Changes))
foreach (var (labels, changes) in groupedEntry.Select(c => (c.Labels, c.Changes)))
{
var text = new RichTextLabel();
text.SetMessage(FormattedMessage.FromUnformatted(change.Message));
ChangelogBody.AddChild(new BoxContainer
foreach (var change in changes)
{
Orientation = LayoutOrientation.Horizontal,
Margin = new Thickness(14, 1, 10, 2),
Children =
{
GetIcon(change.Type),
text
}
});
var entry = new ChangelogEntry();
entry.SetText(FormattedMessage.FromUnformatted(change.Message));
entry.SetIcons(change.Type, labels.Contains(_experimentalString));
ChangelogBody.AddChild(entry);
}
}
}
}
}
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)
};
}
}

View file

@ -0,0 +1,4 @@
- files: ["test-tube.svg"]
license: "CC-BY-4.0"
copyright: "Solar Icons"
source: "https://www.svgrepo.com/svg/526351/test-tube-minimalistic"

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="-297 389 16 16" style="enable-background:new -297 389 16 16;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="SVGRepo_bgCarrier">
</g>
<g id="SVGRepo_tracerCarrier">
</g>
<g id="SVGRepo_iconCarrier">
<path class="st0" d="M-287.3,390.2c-0.2-0.2-0.6-0.2-0.8,0c-0.2,0.2-0.2,0.6,0,0.8l0.5,0.5l-5.1,5.1l0.5,0.1c1,0.1,1.8,0.9,1.9,2
c0,0.4,0.3,0.7,0.6,0.9l1.4,0.6l4.6-4.6l0.6,0.6c0.2,0.2,0.6,0.2,0.8,0c0.2-0.2,0.2-0.6,0-0.8L-287.3,390.2z"/>
<path class="st0" d="M-295.2,399.1l1.5-1.5l1.4,0.2c0.5,0.1,0.9,0.5,1,1c0.1,0.8,0.6,1.5,1.3,1.8l1,0.4l-2.2,2.2
c-1.1,1.1-2.9,1.1-4,0C-296.3,402.1-296.3,400.2-295.2,399.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

View file

@ -0,0 +1,2 @@
sample:
filter: true

View file

@ -55,6 +55,9 @@ TYPES_TO_EMOJI = {
"Tweak": "⚒️"
}
EXPERIMENTAL_LABEL = "Intent: Experimental"
EXPERIMENTAL_EMOJI = "🧪"
XML_NS = "https://spacestation14.com/changelog_rss"
XML_NS_B = f"{{{XML_NS}}}"
@ -181,6 +184,8 @@ def generate_description_for_entries(entries: List[Any]) -> str:
for entry in sorted(group, key=lambda x: x["time"]):
for change in entry["changes"]:
emoji = TYPES_TO_EMOJI.get(change["type"], "")
if EXPERIMENTAL_LABEL in entry["labels"]
emoji = f"{emoji}{EXPERIMENTAL_EMOJI}"
msg = change["message"]
desc.write(f"<li>{emoji} {html.escape(msg)}</li>")

View file

@ -31,6 +31,9 @@ CHANGELOG_FILE = "Resources/Changelog/Changelog.yml"
TYPES_TO_EMOJI = {"Fix": "🐛", "Add": "🆕", "Remove": "❌", "Tweak": "⚒️"}
EXPERIMENTAL_LABEL = "Intent: Experimental"
EXPERIMENTAL_EMOJI = "🧪"
ChangelogEntry = dict[str, Any]
@ -239,6 +242,9 @@ def changelog_entries_to_message_lines(entries: Iterable[ChangelogEntry]) -> lis
emoji = TYPES_TO_EMOJI.get(change["type"], "❓")
message = change["message"]
if EXPERIMENTAL_LABEL in entry["labels"]
emoji = f"{emoji}{EXPERIMENTAL_EMOJI}"
message_lines.append(create_change_line(emoji, message, url))
return message_lines