// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Diagnostics; using System.Net; using System.Text.RegularExpressions; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osuTK; using osuTK.Graphics; using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Overlays.Changelog { public partial class ChangelogEntry : FillFlowContainer { private readonly APIChangelogEntry entry; [Resolved] private OsuColour colours { get; set; } = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; [Resolved] private ILinkHandler? linkHandler { get; set; } private FontUsage fontLarge; private FontUsage fontMedium; public ChangelogEntry(APIChangelogEntry entry) { this.entry = entry; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Direction = FillDirection.Vertical; } [BackgroundDependencyLoader] private void load() { fontLarge = OsuFont.GetFont(size: 16); fontMedium = OsuFont.GetFont(size: 12); Children = new[] { createTitle(), createMessage() }; } private Drawable createTitle() { var entryColour = entry.Major ? colours.YellowLight : Color4.White; LinkFlowContainer title; var titleContainer = new Container { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Margin = new MarginPadding { Vertical = 5 }, Children = new Drawable[] { new SpriteIcon { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreRight, Size = new Vector2(10), Icon = getIconForChangelogEntry(entry.Type), Colour = entryColour.Opacity(0.5f), Margin = new MarginPadding { Right = 5 }, }, title = new LinkFlowContainer { Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, TextAnchor = Anchor.BottomLeft, } } }; if (string.IsNullOrEmpty(entry.Url)) { title.AddText(entry.Title, t => { t.Font = fontLarge; t.Colour = entryColour; }); } else { title.AddLink(entry.Title, () => linkHandler?.HandleLink(entry.Url), entry.Url, t => { t.Font = fontLarge; }); } if (!string.IsNullOrEmpty(entry.Repository) && !string.IsNullOrEmpty(entry.GithubUrl)) addRepositoryReference(title, entryColour); if (entry.GithubUser != null) addGithubAuthorReference(title, entryColour); return titleContainer; } private void addRepositoryReference(LinkFlowContainer title, Color4 entryColour) { Debug.Assert(!string.IsNullOrEmpty(entry.Repository)); Debug.Assert(!string.IsNullOrEmpty(entry.GithubUrl)); title.AddText(" (", t => { t.Font = fontLarge; t.Colour = entryColour; }); title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, t => { t.Font = fontLarge; t.Colour = entryColour; }); title.AddText(")", t => { t.Font = fontLarge; t.Colour = entryColour; }); } private void addGithubAuthorReference(LinkFlowContainer title, Color4 entryColour) { title.AddText("by ", t => { t.Font = fontMedium; t.Colour = entryColour; t.Padding = new MarginPadding { Left = 10 }; }); if (entry.GithubUser.UserId != null) { title.AddUserLink(new APIUser { Username = entry.GithubUser.OsuUsername, Id = entry.GithubUser.UserId.Value }, t => { t.Font = fontMedium; t.Colour = entryColour; }); } else if (entry.GithubUser.GithubUrl != null) { title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => { t.Font = fontMedium; t.Colour = entryColour; }); } else { title.AddText(entry.GithubUser.DisplayName, t => { t.Font = fontMedium; t.Colour = entryColour; }); } } private Drawable createMessage() { if (string.IsNullOrEmpty(entry.MessageHtml)) return Empty(); var message = new TextFlowContainer { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, }; // todo: use markdown parsing once API returns markdown message.AddText(WebUtility.HtmlDecode(Regex.Replace(entry.MessageHtml, @"<(.|\n)*?>", string.Empty)), t => { t.Font = fontMedium; t.Colour = colourProvider.Foreground1; }); return message; } private static IconUsage getIconForChangelogEntry(ChangelogEntryType entryType) { // compare: https://github.com/ppy/osu-web/blob/master/resources/assets/coffee/react/_components/changelog-entry.coffee#L8-L11 switch (entryType) { case ChangelogEntryType.Add: return FontAwesome.Solid.Plus; case ChangelogEntryType.Fix: return FontAwesome.Solid.Check; case ChangelogEntryType.Misc: return FontAwesome.Regular.Circle; default: throw new ArgumentOutOfRangeException(nameof(entryType), $"Unrecognised entry type {entryType}"); } } } }