diff --git a/osu.Game.Tests/Visual/TestCaseBadgeContainer.cs b/osu.Game.Tests/Visual/TestCaseBadgeContainer.cs new file mode 100644 index 000000000000..8177e2e2723d --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseBadgeContainer.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Overlays.Profile.Header; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseBadgeContainer : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] { typeof(BadgeContainer) }; + + public TestCaseBadgeContainer() + { + BadgeContainer badgeContainer; + + Child = badgeContainer = new BadgeContainer + { + RelativeSizeAxes = Axes.Both + }; + + AddStep("Show 1 badge", () => badgeContainer.ShowBadges(new[] + { + new Badge + { + AwardedAt = DateTimeOffset.Now, + Description = "Appreciates compasses", + ImageUrl = "https://assets.ppy.sh/profile-badges/mg2018-1star.png", + } + })); + + AddStep("Show 2 badges", () => badgeContainer.ShowBadges(new[] + { + new Badge + { + AwardedAt = DateTimeOffset.Now, + Description = "Contributed to osu!lazer testing", + ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.png", + }, + new Badge + { + AwardedAt = DateTimeOffset.Now, + Description = "Appreciates compasses", + ImageUrl = "https://assets.ppy.sh/profile-badges/mg2018-1star.png", + } + })); + + AddStep("Show many badges", () => badgeContainer.ShowBadges(Enumerable.Range(1, 20).Select(i => new Badge + { + AwardedAt = DateTimeOffset.Now, + Description = $"Contributed to osu!lazer testing {i} times", + ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg", + }).ToArray())); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseRankGraph.cs b/osu.Game.Tests/Visual/TestCaseRankGraph.cs index 45f6651537fe..f5558620ad35 100644 --- a/osu.Game.Tests/Visual/TestCaseRankGraph.cs +++ b/osu.Game.Tests/Visual/TestCaseRankGraph.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Overlays.Profile; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using OpenTK; @@ -11,6 +10,7 @@ using System; using NUnit.Framework; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Profile.Header; using osu.Game.Users; namespace osu.Game.Tests.Visual diff --git a/osu.Game.Tests/Visual/TestCaseUserProfile.cs b/osu.Game.Tests/Visual/TestCaseUserProfile.cs index b060b9f2f831..0fdc01a9747b 100644 --- a/osu.Game.Tests/Visual/TestCaseUserProfile.cs +++ b/osu.Game.Tests/Visual/TestCaseUserProfile.cs @@ -8,6 +8,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Profile; +using osu.Game.Overlays.Profile.Header; using osu.Game.Users; namespace osu.Game.Tests.Visual @@ -23,6 +24,7 @@ public class TestCaseUserProfile : OsuTestCase typeof(UserProfileOverlay), typeof(RankGraph), typeof(LineGraph), + typeof(BadgeContainer) }; public TestCaseUserProfile() @@ -53,6 +55,15 @@ protected override void LoadComplete() { Mode = @"osu", Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray() + }, + Badges = new[] + { + new Badge + { + AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569), + Description = "Outstanding help by being a voluntary test subject.", + ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg" + } } }, false)); diff --git a/osu.Game/Overlays/Profile/Header/BadgeContainer.cs b/osu.Game/Overlays/Profile/Header/BadgeContainer.cs new file mode 100644 index 000000000000..291db45e97fd --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/BadgeContainer.cs @@ -0,0 +1,195 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Users; +using OpenTK; + +namespace osu.Game.Overlays.Profile.Header +{ + public class BadgeContainer : Container + { + private static readonly Vector2 badge_size = new Vector2(86, 40); + private static readonly MarginPadding outer_padding = new MarginPadding(3); + + private OsuSpriteText badgeCountText; + private FillFlowContainer badgeFlowContainer; + private FillFlowContainer outerBadgeContainer; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Child = new Container + { + Masking = true, + CornerRadius = 4, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray3 + }, + outerBadgeContainer = new OuterBadgeContainer(onOuterHover, onOuterHoverLost) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Direction = FillDirection.Vertical, + Padding = outer_padding, + Width = DrawableBadge.DRAWABLE_BADGE_SIZE.X + outer_padding.TotalHorizontal, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + badgeCountText = new OsuSpriteText + { + Alpha = 0, + TextSize = 12, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = "Exo2.0-Regular" + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + Child = badgeFlowContainer = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + } + } + } + }, + } + }; + + Scheduler.AddDelayed(rotateBadges, 3000, true); + } + + private void rotateBadges() + { + if (outerBadgeContainer.IsHovered) return; + + visibleBadge = (visibleBadge + 1) % badgeCount; + + badgeFlowContainer.MoveToX(-DrawableBadge.DRAWABLE_BADGE_SIZE.X * visibleBadge, 500, Easing.InOutQuad); + } + + private int visibleBadge; + private int badgeCount; + + public void ShowBadges(Badge[] badges) + { + switch (badges.Length) + { + case 0: + Hide(); + return; + case 1: + badgeCountText.Hide(); + break; + default: + badgeCountText.Show(); + badgeCountText.Text = $"{badges.Length} badges"; + break; + } + + Show(); + badgeCount = badges.Length; + visibleBadge = 0; + + badgeFlowContainer.Clear(); + foreach (var badge in badges) + { + LoadComponentAsync(new DrawableBadge(badge) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, badgeFlowContainer.Add); + } + } + + private void onOuterHover() + { + badgeFlowContainer.ClearTransforms(); + badgeFlowContainer.X = 0; + badgeFlowContainer.Direction = FillDirection.Full; + outerBadgeContainer.AutoSizeAxes = Axes.Both; + + badgeFlowContainer.MaximumSize = new Vector2(ChildSize.X, float.MaxValue); + } + + private void onOuterHoverLost() + { + badgeFlowContainer.X = -DrawableBadge.DRAWABLE_BADGE_SIZE.X * visibleBadge; + badgeFlowContainer.Direction = FillDirection.Horizontal; + outerBadgeContainer.AutoSizeAxes = Axes.Y; + outerBadgeContainer.Width = DrawableBadge.DRAWABLE_BADGE_SIZE.X + outer_padding.TotalHorizontal; + } + + private class OuterBadgeContainer : FillFlowContainer + { + private readonly Action hoverAction; + private readonly Action hoverLostAction; + + public OuterBadgeContainer(Action hoverAction, Action hoverLostAction) + { + this.hoverAction = hoverAction; + this.hoverLostAction = hoverLostAction; + } + + protected override bool OnHover(InputState state) + { + hoverAction(); + return true; + } + + protected override void OnHoverLost(InputState state) => hoverLostAction(); + } + + private class DrawableBadge : Container, IHasTooltip + { + public static readonly Vector2 DRAWABLE_BADGE_SIZE = badge_size + outer_padding.Total; + + private readonly Badge badge; + + public DrawableBadge(Badge badge) + { + this.badge = badge; + Padding = outer_padding; + Size = DRAWABLE_BADGE_SIZE; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Child = new Sprite + { + FillMode = FillMode.Fit, + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(badge.ImageUrl), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Child.FadeInFromZero(200); + } + + public string TooltipText => badge.Description; + } + } +} diff --git a/osu.Game/Overlays/Profile/RankGraph.cs b/osu.Game/Overlays/Profile/Header/RankGraph.cs similarity index 99% rename from osu.Game/Overlays/Profile/RankGraph.cs rename to osu.Game/Overlays/Profile/Header/RankGraph.cs index 72dd4352f662..2c70507536a7 100644 --- a/osu.Game/Overlays/Profile/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/RankGraph.cs @@ -2,9 +2,10 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.Linq; -using OpenTK; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -14,10 +15,9 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Users; -using System.Collections.Generic; -using osu.Framework.Configuration; +using OpenTK; -namespace osu.Game.Overlays.Profile +namespace osu.Game.Overlays.Profile.Header { public class RankGraph : Container { diff --git a/osu.Game/Overlays/Profile/SupporterIcon.cs b/osu.Game/Overlays/Profile/Header/SupporterIcon.cs similarity index 98% rename from osu.Game/Overlays/Profile/SupporterIcon.cs rename to osu.Game/Overlays/Profile/Header/SupporterIcon.cs index e8d52bf50e32..37ad63464ccf 100644 --- a/osu.Game/Overlays/Profile/SupporterIcon.cs +++ b/osu.Game/Overlays/Profile/Header/SupporterIcon.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -9,8 +8,9 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using OpenTK; -namespace osu.Game.Overlays.Profile +namespace osu.Game.Overlays.Profile.Header { public class SupporterIcon : CircularContainer, IHasTooltip { diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 4a88431cc46d..ec0e45d5ca2f 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays.Profile.Header; using osu.Game.Users; namespace osu.Game.Overlays.Profile @@ -35,6 +36,7 @@ public class ProfileHeader : Container private readonly GradeBadge gradeSSPlus, gradeSS, gradeSPlus, gradeS, gradeA; private readonly Box colourBar; private readonly DrawableFlag countryFlag; + private readonly BadgeContainer badgeContainer; private const float cover_height = 350; private const float info_height = 150; @@ -42,6 +44,7 @@ public class ProfileHeader : Container private const float avatar_size = 110; private const float level_position = 30; private const float level_height = 60; + private const float stats_width = 280; public ProfileHeader(User user) { @@ -66,9 +69,9 @@ public ProfileHeader(User user) { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - X = UserProfileOverlay.CONTENT_X_MARGIN, - Y = -20, - AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN, Bottom = 20, Right = stats_width + UserProfileOverlay.CONTENT_X_MARGIN }, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, Children = new Drawable[] { new UpdateableAvatar @@ -116,7 +119,15 @@ public ProfileHeader(User user) Height = 20 } } - } + }, + badgeContainer = new BadgeContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Bottom = 5 }, + Alpha = 0, + }, } }, colourBar = new Box @@ -156,7 +167,7 @@ public ProfileHeader(User user) { X = -UserProfileOverlay.CONTENT_X_MARGIN, RelativeSizeAxes = Axes.Y, - Width = 280, + Width = stats_width, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Children = new Drawable[] @@ -417,6 +428,8 @@ private void loadUser() rankGraph.User.Value = user; } + + badgeContainer.ShowBadges(user.Badges); } private void tryAddInfoRightLine(FontAwesome icon, string str, string url = null) diff --git a/osu.Game/Users/Badge.cs b/osu.Game/Users/Badge.cs new file mode 100644 index 000000000000..25ef8ffdf473 --- /dev/null +++ b/osu.Game/Users/Badge.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using Newtonsoft.Json; + +namespace osu.Game.Users +{ + public class Badge + { + [JsonProperty("awarded_at")] + public DateTimeOffset AwardedAt; + + [JsonProperty("description")] + public string Description; + + [JsonProperty("image_url")] + public string ImageUrl; + } +} diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 441e4ffd28b5..b983b639f0b1 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -137,6 +137,9 @@ public class RankHistoryData [JsonProperty(@"rankHistory")] public RankHistoryData RankHistory; + [JsonProperty("badges")] + public Badge[] Badges; + public override string ToString() => Username; } } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 424d7b7a8fe4..bcb91c1955d7 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -17,7 +17,7 @@ using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.Containers; -using osu.Game.Overlays.Profile; +using osu.Game.Overlays.Profile.Header; namespace osu.Game.Users {