diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index 2f2594d5ed..8bfee02310 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -31,22 +31,51 @@ namespace osu.Game.Tests.Visual.Beatmaps normal.HasVideo = true; normal.HasStoryboard = true; + var withStatistics = CreateAPIBeatmapSet(Ruleset.Value); + withStatistics.Title = withStatistics.TitleUnicode = "play favourite stats"; + withStatistics.Status = BeatmapSetOnlineStatus.Approved; + withStatistics.FavouriteCount = 284_239; + withStatistics.PlayCount = 999_001; + withStatistics.Ranked = DateTimeOffset.Now.AddDays(-45); + withStatistics.HypeStatus = new BeatmapSetHypeStatus + { + Current = 34, + Required = 5 + }; + withStatistics.NominationStatus = new BeatmapSetNominationStatus + { + Current = 1, + Required = 2 + }; + var undownloadable = getUndownloadableBeatmapSet(); + undownloadable.LastUpdated = DateTimeOffset.Now.AddYears(-1); var someDifficulties = getManyDifficultiesBeatmapSet(11); + someDifficulties.Title = someDifficulties.TitleUnicode = "favourited"; someDifficulties.Title = someDifficulties.TitleUnicode = "some difficulties"; someDifficulties.Status = BeatmapSetOnlineStatus.Qualified; + someDifficulties.HasFavourited = true; + someDifficulties.FavouriteCount = 1; + someDifficulties.NominationStatus = new BeatmapSetNominationStatus + { + Current = 2, + Required = 2 + }; var manyDifficulties = getManyDifficultiesBeatmapSet(100); manyDifficulties.Status = BeatmapSetOnlineStatus.Pending; var explicitMap = CreateAPIBeatmapSet(Ruleset.Value); + explicitMap.Title = someDifficulties.TitleUnicode = "explicit beatmap"; explicitMap.HasExplicitContent = true; var featuredMap = CreateAPIBeatmapSet(Ruleset.Value); + featuredMap.Title = someDifficulties.TitleUnicode = "featured artist beatmap"; featuredMap.TrackId = 1; var explicitFeaturedMap = CreateAPIBeatmapSet(Ruleset.Value); + explicitFeaturedMap.Title = someDifficulties.TitleUnicode = "explicit featured artist"; explicitFeaturedMap.HasExplicitContent = true; explicitFeaturedMap.TrackId = 2; @@ -59,6 +88,7 @@ namespace osu.Game.Tests.Visual.Beatmaps testCases = new[] { normal, + withStatistics, undownloadable, someDifficulties, manyDifficulties, diff --git a/osu.Game/Beatmaps/BeatmapSetHypeStatus.cs b/osu.Game/Beatmaps/BeatmapSetHypeStatus.cs new file mode 100644 index 0000000000..8a576e396a --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapSetHypeStatus.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Beatmaps +{ + /// + /// Contains information about the current hype status of a beatmap set. + /// + public class BeatmapSetHypeStatus + { + /// + /// The current number of hypes that the set has received. + /// + [JsonProperty(@"current")] + public int Current { get; set; } + + /// + /// The number of hypes required so that the set is eligible for nomination. + /// + [JsonProperty(@"required")] + public int Required { get; set; } + } +} diff --git a/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs b/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs new file mode 100644 index 0000000000..6a19616a97 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Beatmaps +{ + /// + /// Contains information about the current nomination status of a beatmap set. + /// + public class BeatmapSetNominationStatus + { + /// + /// The current number of nominations that the set has received. + /// + [JsonProperty(@"current")] + public int Current { get; set; } + + /// + /// The number of nominations required so that the map is eligible for qualification. + /// + [JsonProperty(@"required")] + public int Required { get; set; } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 8136ebbd70..c53c1abd8d 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -8,6 +9,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Beatmaps.Drawables.Cards.Statistics; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -40,6 +42,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards private GridContainer titleContainer; private GridContainer artistContainer; + private FillFlowContainer statisticsContainer; [Resolved] private OverlayColourProvider colourProvider { get; set; } @@ -176,6 +179,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); d.AddUserLink(beatmapSet.Author); }), + statisticsContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Alpha = 0, + ChildrenEnumerable = createStatistics() + } } }, new FillFlowContainer @@ -265,6 +277,24 @@ namespace osu.Game.Beatmaps.Drawables.Cards return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); } + private IEnumerable createStatistics() + { + if (beatmapSet.HypeStatus != null) + yield return new HypesStatistic(beatmapSet.HypeStatus); + + // web does not show nominations unless hypes are also present. + // see: https://github.com/ppy/osu-web/blob/8ed7d071fd1d3eaa7e43cf0e4ff55ca2fef9c07c/resources/assets/lib/beatmapset-panel.tsx#L443 + if (beatmapSet.HypeStatus != null && beatmapSet.NominationStatus != null) + yield return new NominationsStatistic(beatmapSet.NominationStatus); + + yield return new FavouritesStatistic(beatmapSet); + yield return new PlayCountStatistic(beatmapSet); + + var dateStatistic = BeatmapCardDateStatistic.CreateFor(beatmapSet); + if (dateStatistic != null) + yield return dateStatistic; + } + private void updateState() { float targetWidth = width - height; @@ -275,6 +305,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards mainContentBackground.Dimmed.Value = IsHovered; leftCover.FadeColour(IsHovered ? OsuColour.Gray(0.2f) : Color4.White, TRANSITION_DURATION, Easing.OutQuint); + statisticsContainer.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardDateStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardDateStatistic.cs new file mode 100644 index 0000000000..8f2c4b538c --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardDateStatistic.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; + +namespace osu.Game.Beatmaps.Drawables.Cards.Statistics +{ + public class BeatmapCardDateStatistic : BeatmapCardStatistic + { + private readonly DateTimeOffset dateTime; + + private BeatmapCardDateStatistic(DateTimeOffset dateTime) + { + this.dateTime = dateTime; + + Icon = FontAwesome.Regular.CheckCircle; + Text = dateTime.ToLocalisableString(@"d MMM yyyy"); + } + + public override object TooltipContent => dateTime; + public override ITooltip GetCustomTooltip() => new DateTooltip(); + + public static BeatmapCardDateStatistic? CreateFor(IBeatmapSetOnlineInfo beatmapSetInfo) + { + var displayDate = displayDateFor(beatmapSetInfo); + + if (displayDate == null) + return null; + + return new BeatmapCardDateStatistic(displayDate.Value); + } + + private static DateTimeOffset? displayDateFor(IBeatmapSetOnlineInfo beatmapSetInfo) + { + // reference: https://github.com/ppy/osu-web/blob/ef432c11719fd1207bec5f9194b04f0033bdf02c/resources/assets/lib/beatmapset-panel.tsx#L36-L44 + switch (beatmapSetInfo.Status) + { + case BeatmapSetOnlineStatus.Ranked: + case BeatmapSetOnlineStatus.Approved: + case BeatmapSetOnlineStatus.Loved: + case BeatmapSetOnlineStatus.Qualified: + return beatmapSetInfo.Ranked; + + default: + return beatmapSetInfo.LastUpdated; + } + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs new file mode 100644 index 0000000000..f46926284f --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Beatmaps.Drawables.Cards.Statistics +{ + /// + /// A single statistic shown on a beatmap card. + /// + public abstract class BeatmapCardStatistic : CompositeDrawable, IHasTooltip, IHasCustomTooltip + { + protected IconUsage Icon + { + get => spriteIcon.Icon; + set => spriteIcon.Icon = value; + } + + protected LocalisableString Text + { + get => spriteText.Text; + set => spriteText.Text = value; + } + + public LocalisableString TooltipText { get; protected set; } + + private readonly SpriteIcon spriteIcon; + private readonly OsuSpriteText spriteText; + + protected BeatmapCardStatistic() + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + spriteIcon = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(10), + Margin = new MarginPadding { Top = 1 } + }, + spriteText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.Default.With(size: 14) + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + spriteIcon.Colour = colourProvider.Content2; + } + + #region Tooltip implementation + + public virtual ITooltip GetCustomTooltip() => null; + public virtual object TooltipContent => null; + + #endregion + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/FavouritesStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/FavouritesStatistic.cs new file mode 100644 index 0000000000..7b3286ddcc --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/FavouritesStatistic.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Humanizer; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; + +namespace osu.Game.Beatmaps.Drawables.Cards.Statistics +{ + /// + /// Shows the number of favourites that a beatmap set has received. + /// + public class FavouritesStatistic : BeatmapCardStatistic + { + public FavouritesStatistic(IBeatmapSetOnlineInfo onlineInfo) + { + Icon = onlineInfo.HasFavourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart; + Text = onlineInfo.FavouriteCount.ToMetric(decimals: 1); + TooltipText = BeatmapsStrings.PanelFavourites(onlineInfo.FavouriteCount.ToLocalisableString(@"N0")); + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/HypesStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/HypesStatistic.cs new file mode 100644 index 0000000000..3fe31c7a41 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/HypesStatistic.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; + +namespace osu.Game.Beatmaps.Drawables.Cards.Statistics +{ + /// + /// Shows the number of current hypes that a map has received, as well as the number of hypes required for nomination. + /// + public class HypesStatistic : BeatmapCardStatistic + { + public HypesStatistic(BeatmapSetHypeStatus hypeStatus) + { + Icon = FontAwesome.Solid.Bullhorn; + Text = hypeStatus.Current.ToLocalisableString(); + TooltipText = BeatmapsStrings.HypeRequiredText(hypeStatus.Current.ToLocalisableString(), hypeStatus.Required.ToLocalisableString()); + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs new file mode 100644 index 0000000000..f09269a615 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; + +namespace osu.Game.Beatmaps.Drawables.Cards.Statistics +{ + /// + /// Shows the number of current nominations that a map has received, as well as the number of nominations required for qualification. + /// + public class NominationsStatistic : BeatmapCardStatistic + { + public NominationsStatistic(BeatmapSetNominationStatus nominationStatus) + { + Icon = FontAwesome.Solid.ThumbsUp; + Text = nominationStatus.Current.ToLocalisableString(); + TooltipText = BeatmapsStrings.NominationsRequiredText(nominationStatus.Current.ToLocalisableString(), nominationStatus.Required.ToLocalisableString()); + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/PlayCountStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/PlayCountStatistic.cs new file mode 100644 index 0000000000..d8f0c36bd9 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/PlayCountStatistic.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Humanizer; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; + +namespace osu.Game.Beatmaps.Drawables.Cards.Statistics +{ + /// + /// Shows the number of times the given beatmap set has been played. + /// + public class PlayCountStatistic : BeatmapCardStatistic + { + public PlayCountStatistic(IBeatmapSetOnlineInfo onlineInfo) + { + Icon = FontAwesome.Regular.PlayCircle; + Text = onlineInfo.PlayCount.ToMetric(decimals: 1); + TooltipText = BeatmapsStrings.PanelPlaycount(onlineInfo.PlayCount.ToLocalisableString(@"N0")); + } + } +} diff --git a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs index 6def6ec21d..2982cf9c3a 100644 --- a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs @@ -102,5 +102,19 @@ namespace osu.Game.Beatmaps /// Total vote counts of user ratings on a scale of 0..10 where 0 is unused (probably will be fixed at API?). /// int[]? Ratings { get; } + + /// + /// Contains the current hype status of the beatmap set. + /// Non-null only for , , and sets. + /// + /// + /// See: https://github.com/ppy/osu-web/blob/93930cd02cfbd49724929912597c727c9fbadcd1/app/Models/Beatmapset.php#L155 + /// + BeatmapSetHypeStatus? HypeStatus { get; } + + /// + /// Contains the current nomination status of the beatmap set. + /// + BeatmapSetNominationStatus? NominationStatus { get; } } } diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 1ff039a6b4..168e9d5d51 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -61,6 +61,12 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"track_id")] public int? TrackId { get; set; } + [JsonProperty(@"hype")] + public BeatmapSetHypeStatus? HypeStatus { get; set; } + + [JsonProperty(@"nominations_summary")] + public BeatmapSetNominationStatus? NominationStatus { get; set; } + public string Title { get; set; } = string.Empty; [JsonProperty("title_unicode")]