diff --git a/osu.Desktop.VisualTests/Tests/TestCaseUserProfile.cs b/osu.Desktop.VisualTests/Tests/TestCaseUserProfile.cs new file mode 100644 index 0000000000..d7a2c8e47d --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseUserProfile.cs @@ -0,0 +1,64 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Testing; +using osu.Game.Overlays; +using osu.Game.Users; + +namespace osu.Desktop.VisualTests.Tests +{ + internal class TestCaseUserProfile : TestCase + { + public override string Description => "Tests user's profile page."; + + public TestCaseUserProfile() + { + var profile = new UserProfileOverlay(); + Add(profile); + + AddStep("Show offline dummy", () => profile.ShowUser(new User + { + Username = @"Somebody", + Id = 1, + Country = new Country { FullName = @"Alien" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", + JoinDate = DateTimeOffset.Now.AddDays(-1), + LastVisit = DateTimeOffset.Now, + Age = 1, + ProfileOrder = new[] { "me" }, + CountryRank = 1, + Statistics = new UserStatistics + { + Rank = 2148, + PP = 4567.89m + }, + AllRankHistories = new User.RankHistories + { + Osu = new User.RankHistory + { + Mode = @"osu", + Data = Enumerable.Range(2345,45).Concat(Enumerable.Range(2109,40)).ToArray() + } + } + }, false)); + AddStep("Show ppy", () => profile.ShowUser(new User + { + Username = @"peppy", + Id = 2, + Country = new Country { FullName = @"Australia", FlagName = @"AU" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg" + })); + AddStep("Show flyte", () => profile.ShowUser(new User + { + Username = @"flyte", + Id = 3103765, + Country = new Country { FullName = @"Japan", FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + })); + AddStep("Hide", profile.Hide); + AddStep("Show without reload", profile.Show); + } + } +} diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index b65bab0d17..4974f0c0d1 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -216,6 +216,7 @@ + diff --git a/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs new file mode 100644 index 0000000000..143f38ced4 --- /dev/null +++ b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Graphics.Containers +{ + public class OsuTextFlowContainer : TextFlowContainer + { + public OsuTextFlowContainer(Action defaultCreationParameters = null) : base(defaultCreationParameters) + { + } + + protected override SpriteText CreateSpriteText() => new OsuSpriteText(); + + public void AddIcon(FontAwesome icon, Action creationParameters = null) => AddText(((char)icon).ToString(), creationParameters); + } +} diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 8ae619cdc0..6dadf0b0b0 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Configuration; using osu.Framework.Graphics; @@ -13,11 +12,15 @@ namespace osu.Game.Graphics.Containers /// /// A container that can scroll to each section inside it. /// - public class SectionsContainer : Container + public class SectionsContainer : Container + where T : Drawable { - private Drawable expandableHeader, fixedHeader, footer; - public readonly ScrollContainer ScrollContainer; - private readonly Container sectionsContainer; + private Drawable expandableHeader, fixedHeader, footer, headerBackground; + private readonly ScrollContainer scrollContainer; + private readonly Container headerBackgroundContainer; + private readonly FlowContainer scrollContentContainer; + + protected override Container Content => scrollContentContainer; public Drawable ExpandableHeader { @@ -26,12 +29,11 @@ namespace osu.Game.Graphics.Containers { if (value == expandableHeader) return; - if (expandableHeader != null) - Remove(expandableHeader); + expandableHeader?.Expire(); expandableHeader = value; if (value == null) return; - Add(expandableHeader); + AddInternal(expandableHeader); lastKnownScroll = float.NaN; } } @@ -43,12 +45,11 @@ namespace osu.Game.Graphics.Containers { if (value == fixedHeader) return; - if (fixedHeader != null) - Remove(fixedHeader); + fixedHeader?.Expire(); fixedHeader = value; if (value == null) return; - Add(fixedHeader); + AddInternal(fixedHeader); lastKnownScroll = float.NaN; } } @@ -61,69 +62,84 @@ namespace osu.Game.Graphics.Containers if (value == footer) return; if (footer != null) - ScrollContainer.Remove(footer); + scrollContainer.Remove(footer); footer = value; if (value == null) return; footer.Anchor |= Anchor.y2; footer.Origin |= Anchor.y2; - ScrollContainer.Add(footer); + scrollContainer.Add(footer); lastKnownScroll = float.NaN; } } - public Bindable SelectedSection { get; } = new Bindable(); - - protected virtual Container CreateScrollContentContainer() - => new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both - }; - - private List sections = new List(); - public IEnumerable Sections + public Drawable HeaderBackground { - get { return sections; } + get { return headerBackground; } set { - foreach (var section in sections) - sectionsContainer.Remove(section); + if (value == headerBackground) return; - sections = value.ToList(); - if (sections.Count == 0) return; + headerBackgroundContainer.Clear(); + headerBackground = value; + if (value == null) return; + + headerBackgroundContainer.Add(headerBackground); - sectionsContainer.AddRange(sections); - SelectedSection.Value = sections[0]; lastKnownScroll = float.NaN; } } + public Bindable SelectedSection { get; } = new Bindable(); + + protected virtual FlowContainer CreateScrollContentContainer() + => new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }; + + public override void Add(T drawable) + { + base.Add(drawable); + lastKnownScroll = float.NaN; + headerHeight = float.NaN; + footerHeight = float.NaN; + } + private float headerHeight, footerHeight; private readonly MarginPadding originalSectionsMargin; private void updateSectionsMargin() { - if (sections.Count == 0) return; + if (!Children.Any()) return; var newMargin = originalSectionsMargin; newMargin.Top += headerHeight; newMargin.Bottom += footerHeight; - sectionsContainer.Margin = newMargin; + scrollContentContainer.Margin = newMargin; } public SectionsContainer() { - Add(ScrollContainer = new OsuScrollContainer + AddInternal(scrollContainer = new ScrollContainer { RelativeSizeAxes = Axes.Both, - Masking = false, - Children = new Drawable[] { sectionsContainer = CreateScrollContentContainer() } + Masking = true, + ScrollbarVisible = false, + Children = new Drawable[] { scrollContentContainer = CreateScrollContentContainer() }, + Depth = float.MaxValue }); - originalSectionsMargin = sectionsContainer.Margin; + AddInternal(headerBackgroundContainer = new Container + { + RelativeSizeAxes = Axes.X, + Depth = float.MaxValue / 2 + }); + originalSectionsMargin = scrollContentContainer.Margin; } - public void ScrollTo(Drawable section) => ScrollContainer.ScrollTo(ScrollContainer.GetChildPosInContent(section) - FixedHeader.BoundingBox.Height); + public void ScrollTo(Drawable section) => scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0)); private float lastKnownScroll; protected override void UpdateAfterChildren() @@ -139,25 +155,30 @@ namespace osu.Game.Graphics.Containers updateSectionsMargin(); } - float currentScroll = Math.Max(0, ScrollContainer.Current); + float currentScroll = scrollContainer.Current; + if (currentScroll != lastKnownScroll) { lastKnownScroll = currentScroll; - if (expandableHeader != null && fixedHeader != null) + if (ExpandableHeader != null && FixedHeader != null) { - float offset = Math.Min(expandableHeader.LayoutSize.Y, currentScroll); + float offset = Math.Min(ExpandableHeader.LayoutSize.Y, currentScroll); - expandableHeader.Y = -offset; - fixedHeader.Y = -offset + expandableHeader.LayoutSize.Y; + ExpandableHeader.Y = -offset; + FixedHeader.Y = -offset + ExpandableHeader.LayoutSize.Y; } - Drawable bestMatch = null; - float minDiff = float.MaxValue; + headerBackgroundContainer.Height = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0); + headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0; - foreach (var section in sections) + T bestMatch = null; + float minDiff = float.MaxValue; + float scrollOffset = FixedHeader?.LayoutSize.Y ?? 0; + + foreach (var section in Children) { - float diff = Math.Abs(ScrollContainer.GetChildPosInContent(section) - currentScroll); + float diff = Math.Abs(scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset); if (diff < minDiff) { minDiff = diff; diff --git a/osu.Game/Graphics/UserInterface/LineGraph.cs b/osu.Game/Graphics/UserInterface/LineGraph.cs new file mode 100644 index 0000000000..cd8ba96dc8 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/LineGraph.cs @@ -0,0 +1,102 @@ +// Copyright (c) 2007-2017 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 OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Lines; + +namespace osu.Game.Graphics.UserInterface +{ + public class LineGraph : Container + { + /// + /// Manually set the max value, otherwise will be used. + /// + public float? MaxValue { get; set; } + + /// + /// Manually set the min value, otherwise will be used. + /// + public float? MinValue { get; set; } + + public float ActualMaxValue { get; private set; } = float.NaN; + public float ActualMinValue { get; private set; } = float.NaN; + + private const double transform_duration = 1500; + + /// + /// Hold an empty area if values are less. + /// + public int DefaultValueCount; + + private readonly Container maskingContainer; + private readonly Path path; + + private float[] values; + + /// + /// A list of floats decides position of each line node. + /// + public IEnumerable Values + { + get { return values; } + set + { + values = value.ToArray(); + applyPath(); + maskingContainer.Width = 0; + maskingContainer.ResizeWidthTo(1, transform_duration, EasingTypes.OutQuint); + } + } + + public LineGraph() + { + Add(maskingContainer = new Container + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Child = path = new Path { RelativeSizeAxes = Axes.Both, PathWidth = 1 } + }); + } + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + if ((invalidation & Invalidation.DrawSize) != 0) + applyPath(); + return base.Invalidate(invalidation, source, shallPropagate); + } + + private void applyPath() + { + path.ClearVertices(); + if (values == null) return; + + int count = Math.Max(values.Length, DefaultValueCount); + + float max = values.Max(), min = values.Min(); + if (MaxValue > max) max = MaxValue.Value; + if (MinValue < min) min = MinValue.Value; + + ActualMaxValue = max; + ActualMinValue = min; + + for (int i = 0; i < values.Length; i++) + { + float x = (i + count - values.Length) / (float)(count - 1) * DrawWidth - 1; + float y = GetYPosition(values[i]) * DrawHeight - 1; + // the -1 is for inner offset in path (actually -PathWidth) + path.AddVertex(new Vector2(x, y)); + } + } + + protected float GetYPosition(float value) + { + if (ActualMaxValue == ActualMinValue) return 0; + return (ActualMaxValue - value) / (ActualMaxValue - ActualMinValue); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index df7238a6c6..602aee3c58 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -23,7 +23,7 @@ namespace osu.Game.Graphics.UserInterface protected override TabItem CreateTabItem(T value) => new OsuTabItem(value); - private bool isEnumType => typeof(T).IsEnum; + private static bool isEnumType => typeof(T).IsEnum; public OsuTabControl() { diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs index 7f2e6c9c8c..6b97e54ecd 100644 --- a/osu.Game/Graphics/UserInterface/PageTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Game.Graphics.Sprites; @@ -23,12 +24,14 @@ namespace osu.Game.Graphics.UserInterface Height = 30; } - private class PageTabItem : TabItem + public class PageTabItem : TabItem { private const float transition_duration = 100; private readonly Box box; + protected readonly SpriteText Text; + public PageTabItem(T value) : base(value) { AutoSizeAxes = Axes.X; @@ -36,12 +39,12 @@ namespace osu.Game.Graphics.UserInterface Children = new Drawable[] { - new OsuSpriteText + Text = new OsuSpriteText { Margin = new MarginPadding { Top = 8, Bottom = 8 }, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, - Text = (value as Enum).GetDescription() ?? value.ToString(), + Text = (value as Enum)?.GetDescription() ?? value.ToString(), TextSize = 14, Font = @"Exo2.0-Bold", }, diff --git a/osu.Game/Online/API/Requests/GetUserRequest.cs b/osu.Game/Online/API/Requests/GetUserRequest.cs index 2fd1ee5efc..2e3e7b01c8 100644 --- a/osu.Game/Online/API/Requests/GetUserRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRequest.cs @@ -7,9 +7,9 @@ namespace osu.Game.Online.API.Requests { public class GetUserRequest : APIRequest { - private int? userId; + private long? userId; - public GetUserRequest(int? userId = null) + public GetUserRequest(long? userId = null) { this.userId = userId; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 843861c0da..6bec2cb184 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -45,6 +45,8 @@ namespace osu.Game private SocialOverlay social; + private UserProfileOverlay userProfile; + private Intro intro { get @@ -173,6 +175,7 @@ namespace osu.Game LoadComponentAsync(direct = new DirectOverlay { Depth = -1 }, mainContent.Add); LoadComponentAsync(social = new SocialOverlay { Depth = -1 }, mainContent.Add); LoadComponentAsync(chat = new ChatOverlay { Depth = -1 }, mainContent.Add); + LoadComponentAsync(userProfile = new UserProfileOverlay { Depth = -1 }, mainContent.Add); LoadComponentAsync(settings = new SettingsOverlay { Depth = -1 }, overlayContent.Add); LoadComponentAsync(musicController = new MusicController { @@ -207,6 +210,7 @@ namespace osu.Game Dependencies.Cache(settings); Dependencies.Cache(social); Dependencies.Cache(chat); + Dependencies.Cache(userProfile); Dependencies.Cache(musicController); Dependencies.Cache(notificationManager); Dependencies.Cache(dialogOverlay); @@ -322,6 +326,7 @@ namespace osu.Game chat.State = Visibility.Hidden; direct.State = Visibility.Hidden; social.State = Visibility.Hidden; + userProfile.State = Visibility.Hidden; } else { diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index 790530342f..d9d5a44fd5 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -46,6 +46,7 @@ namespace osu.Game.Overlays settingsSection = new LoginSettings { Padding = new MarginPadding(10), + RequestHide = Hide, }, new Box { diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 69cb9c3464..0a095327b0 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -2,15 +2,16 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; +using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Graphics; -using OpenTK.Graphics; -using osu.Framework.Localisation; -using osu.Framework.Graphics.Sprites; -using System.Collections.Generic; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Music { @@ -77,7 +78,7 @@ namespace osu.Game.Overlays.Music Margin = new MarginPadding { Left = 5 }, Padding = new MarginPadding { Top = 2 }, }, - text = new TextFlowContainer + text = new OsuTextFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs new file mode 100644 index 0000000000..17493d5078 --- /dev/null +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -0,0 +1,483 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Users; + +namespace osu.Game.Overlays.Profile +{ + public class ProfileHeader : Container + { + private readonly OsuTextFlowContainer infoTextLeft, infoTextRight; + private readonly FillFlowContainer scoreText, scoreNumberText; + + private readonly Container coverContainer, chartContainer, supporterTag; + private readonly Sprite levelBadge; + private readonly SpriteText levelText; + private readonly GradeBadge gradeSSPlus, gradeSS, gradeSPlus, gradeS, gradeA; + private readonly Box colourBar; + + private const float cover_height = 350; + private const float info_height = 150; + private const float info_width = 220; + private const float avatar_size = 110; + private const float level_position = 30; + private const float level_height = 60; + + public ProfileHeader(User user) + { + RelativeSizeAxes = Axes.X; + Height = cover_height + info_height; + + Children = new Drawable[] + { + coverContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = cover_height, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + ColourInfo = ColourInfo.GradientVertical(Color4.Black.Opacity(0.1f), Color4.Black.Opacity(0.75f)) + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + X = UserProfileOverlay.CONTENT_X_MARGIN, + Y = -20, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new UpdateableAvatar + { + User = user, + Size = new Vector2(avatar_size), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Masking = true, + CornerRadius = 5, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 4, + }, + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + X = avatar_size + 10, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + supporterTag = new CircularContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Y = -75, + Size = new Vector2(25, 25), + Masking = true, + BorderThickness = 3, + BorderColour = Color4.White, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new TextAwesome + { + Icon = FontAwesome.fa_heart, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + TextSize = 12 + } + } + }, + new OsuSpriteText + { + Text = user.Username, + TextSize = 30, + Font = @"Exo2.0-RegularItalic", + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Y = -48 + }, + new DrawableFlag(user.Country?.FlagName ?? "__") + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Width = 30, + Height = 20 + } + } + } + } + }, + colourBar = new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + X = UserProfileOverlay.CONTENT_X_MARGIN, + Height = 5, + Width = info_width, + Alpha = 0 + } + } + }, + infoTextLeft = new OsuTextFlowContainer(t => + { + t.TextSize = 14; + t.Alpha = 0.8f; + }) + { + X = UserProfileOverlay.CONTENT_X_MARGIN, + Y = cover_height + 20, + Width = info_width, + AutoSizeAxes = Axes.Y, + ParagraphSpacing = 0.8f, + LineSpacing = 0.2f + }, + infoTextRight = new OsuTextFlowContainer(t => + { + t.TextSize = 14; + t.Font = @"Exo2.0-RegularItalic"; + }) + { + X = UserProfileOverlay.CONTENT_X_MARGIN + info_width + 20, + Y = cover_height + 20, + Width = info_width, + AutoSizeAxes = Axes.Y, + ParagraphSpacing = 0.8f, + LineSpacing = 0.2f + }, + new Container + { + X = -UserProfileOverlay.CONTENT_X_MARGIN, + RelativeSizeAxes = Axes.Y, + Width = 280, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + Y = level_position, + Height = level_height, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black.Opacity(0.5f), + RelativeSizeAxes = Axes.Both + }, + levelBadge = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Height = 50, + Width = 50, + Alpha = 0 + }, + levelText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Y = 11, + TextSize = 20 + } + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + Y = cover_height, + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + Height = cover_height - level_height - level_position - 5, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black.Opacity(0.5f), + RelativeSizeAxes = Axes.Both + }, + scoreText = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = 20, Vertical = 18 }, + Spacing = new Vector2(0, 2) + }, + scoreNumberText = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = 20, Vertical = 18 }, + Spacing = new Vector2(0, 2) + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Y = -64, + Spacing = new Vector2(20, 0), + Children = new[] + { + gradeSSPlus = new GradeBadge("SSPlus") { Alpha = 0 }, + gradeSS = new GradeBadge("SS") { Alpha = 0 }, + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Y = -18, + Spacing = new Vector2(20, 0), + Children = new[] + { + gradeSPlus = new GradeBadge("SPlus") { Alpha = 0 }, + gradeS = new GradeBadge("S") { Alpha = 0 }, + gradeA = new GradeBadge("A") { Alpha = 0 }, + } + } + } + }, + chartContainer = new Container + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Height = info_height - 15, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black.Opacity(0.25f), + RelativeSizeAxes = Axes.Both + } + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + levelBadge.Texture = textures.Get(@"Profile/levelbadge"); + } + + private User user; + + public User User + { + get + { + return user; + } + + set + { + user = value; + loadUser(); + } + } + + private void loadUser() + { + coverContainer.Add(new AsyncLoadWrapper(new UserCoverBackground(user) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + OnLoadComplete = d => d.FadeInFromZero(200) + }) + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }); + + if (user.IsSupporter) supporterTag.Show(); + + if (!string.IsNullOrEmpty(user.Colour)) + { + colourBar.Colour = OsuColour.FromHex(user.Colour); + colourBar.Show(); + } + + Action boldItalic = t => + { + t.Font = @"Exo2.0-BoldItalic"; + t.Alpha = 1; + }; + + if (user.Age != null) + { + infoTextLeft.AddText($"{user.Age} years old ", boldItalic); + } + if (user.Country != null) + { + infoTextLeft.AddText("from "); + infoTextLeft.AddText(user.Country.FullName, boldItalic); + } + infoTextLeft.NewParagraph(); + + if (user.JoinDate.ToUniversalTime().Year < 2008) + { + infoTextLeft.AddText("Here since the beginning", boldItalic); + } + else + { + infoTextLeft.AddText("Joined "); + infoTextLeft.AddText(user.JoinDate.LocalDateTime.ToShortDateString(), boldItalic); + } + infoTextLeft.NewLine(); + infoTextLeft.AddText("Last seen "); + infoTextLeft.AddText(user.LastVisit.LocalDateTime.ToShortDateString(), boldItalic); + infoTextLeft.NewParagraph(); + + if (user.PlayStyle?.Length > 0) + { + infoTextLeft.AddText("Plays with "); + infoTextLeft.AddText(string.Join(", ", user.PlayStyle), boldItalic); + } + + tryAddInfoRightLine(FontAwesome.fa_map_marker, user.Location); + tryAddInfoRightLine(FontAwesome.fa_heart_o, user.Intrerests); + tryAddInfoRightLine(FontAwesome.fa_suitcase, user.Occupation); + infoTextRight.NewParagraph(); + if (!string.IsNullOrEmpty(user.Twitter)) + tryAddInfoRightLine(FontAwesome.fa_twitter, "@" + user.Twitter); + tryAddInfoRightLine(FontAwesome.fa_globe, user.Website); + tryAddInfoRightLine(FontAwesome.fa_skype, user.Skype); + + if (user.Statistics != null) + { + levelBadge.Show(); + levelText.Text = user.Statistics.Level.Current.ToString(); + + scoreText.Add(createScoreText("Ranked Score")); + scoreNumberText.Add(createScoreNumberText(user.Statistics.RankedScore.ToString(@"#,0"))); + scoreText.Add(createScoreText("Accuracy")); + scoreNumberText.Add(createScoreNumberText($"{user.Statistics.Accuracy}%")); + scoreText.Add(createScoreText("Play Count")); + scoreNumberText.Add(createScoreNumberText(user.Statistics.PlayCount.ToString(@"#,0"))); + scoreText.Add(createScoreText("Total Score")); + scoreNumberText.Add(createScoreNumberText(user.Statistics.TotalScore.ToString(@"#,0"))); + scoreText.Add(createScoreText("Total Hits")); + scoreNumberText.Add(createScoreNumberText(user.Statistics.TotalHits.ToString(@"#,0"))); + scoreText.Add(createScoreText("Max Combo")); + scoreNumberText.Add(createScoreNumberText(user.Statistics.MaxCombo.ToString(@"#,0"))); + scoreText.Add(createScoreText("Replay Watched by Others")); + scoreNumberText.Add(createScoreNumberText(user.Statistics.ReplayWatched.ToString(@"#,0"))); + + gradeSS.DisplayCount = user.Statistics.GradesCount.SS; + gradeSS.Show(); + gradeS.DisplayCount = user.Statistics.GradesCount.S; + gradeS.Show(); + gradeA.DisplayCount = user.Statistics.GradesCount.A; + gradeA.Show(); + + gradeSPlus.DisplayCount = 0; + gradeSSPlus.DisplayCount = 0; + + chartContainer.Add(new RankChart(user) { RelativeSizeAxes = Axes.Both }); + } + } + + // These could be local functions when C# 7 enabled + + private OsuSpriteText createScoreText(string text) => new OsuSpriteText + { + TextSize = 14, + Text = text + }; + + private OsuSpriteText createScoreNumberText(string text) => new OsuSpriteText + { + TextSize = 14, + Font = @"Exo2.0-Bold", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Text = text + }; + + private void tryAddInfoRightLine(FontAwesome icon, string str) + { + if (string.IsNullOrEmpty(str)) return; + + infoTextRight.AddIcon(icon); + infoTextRight.AddText(" " + str); + infoTextRight.NewLine(); + } + + private class GradeBadge : Container + { + private const float width = 50; + private readonly string grade; + private readonly Sprite badge; + private readonly SpriteText numberText; + + public int DisplayCount + { + set { numberText.Text = value.ToString(@"#,0"); } + } + + public GradeBadge(string grade) + { + this.grade = grade; + Width = width; + Height = 41; + Add(badge = new Sprite + { + Width = width, + Height = 26 + }); + Add(numberText = new SpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + TextSize = 14, + Font = @"Exo2.0-Bold" + }); + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + badge.Texture = textures.Get($"Grades/{grade}"); + } + } + } +} diff --git a/osu.Game/Overlays/Profile/ProfileSection.cs b/osu.Game/Overlays/Profile/ProfileSection.cs new file mode 100644 index 0000000000..2b5084e321 --- /dev/null +++ b/osu.Game/Overlays/Profile/ProfileSection.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using OpenTK.Graphics; + +namespace osu.Game.Overlays.Profile +{ + public abstract class ProfileSection : FillFlowContainer + { + public abstract string Title { get; } + + public abstract string Identifier { get; } + + private readonly FillFlowContainer content; + protected override Container Content => content; + + protected ProfileSection() + { + Direction = FillDirection.Vertical; + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + InternalChildren = new Drawable[] + { + new OsuSpriteText + { + Text = Title, + TextSize = 20, + Font = @"Exo2.0-RegularItalic", + Margin = new MarginPadding + { + Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, + Vertical = 10 + } + }, + content = new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding + { + Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, + Bottom = 20 + } + }, + new Box + { + RelativeSizeAxes = Axes.X, + Height = 1, + Colour = OsuColour.Gray(34), + EdgeSmoothness = new Vector2(1) + } + }; + + // placeholder + Add(new OsuSpriteText + { + Text = @"coming soon!", + TextSize = 16, + Font = @"Exo2.0-Medium", + Colour = Color4.Gray, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Top = 100, Bottom = 100 } + }); + } + } +} diff --git a/osu.Game/Overlays/Profile/RankChart.cs b/osu.Game/Overlays/Profile/RankChart.cs new file mode 100644 index 0000000000..dfd2219e1f --- /dev/null +++ b/osu.Game/Overlays/Profile/RankChart.cs @@ -0,0 +1,177 @@ +// Copyright (c) 2007-2017 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 OpenTK; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Users; + +namespace osu.Game.Overlays.Profile +{ + public class RankChart : Container + { + private readonly SpriteText rankText, performanceText, relativeText; + private readonly RankChartLineGraph graph; + + private readonly int[] ranks; + + private const float primary_textsize = 25, secondary_textsize = 13, padding = 10; + + private readonly User user; + + public RankChart(User user) + { + this.user = user; + + Padding = new MarginPadding { Vertical = padding }; + Children = new Drawable[] + { + rankText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = @"Exo2.0-RegularItalic", + TextSize = primary_textsize + }, + relativeText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = @"Exo2.0-RegularItalic", + Y = 25, + TextSize = secondary_textsize + }, + performanceText = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = @"Exo2.0-RegularItalic", + TextSize = secondary_textsize + }, + graph = new RankChartLineGraph + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + Y = -secondary_textsize, + DefaultValueCount = 90, + BallRelease = updateRankTexts, + BallMove = showHistoryRankTexts + } + }; + + ranks = user.AllRankHistories?.Osu?.Data ?? new[] { user.Statistics.Rank }; + } + + private void updateRankTexts() + { + rankText.Text = user.Statistics.Rank > 0 ? $"#{user.Statistics.Rank:#,0}" : "no rank"; + performanceText.Text = user.Statistics.PP != null ? $"{user.Statistics.PP:#,0}pp" : string.Empty; + relativeText.Text = $"{user.Country?.FullName} #{user.CountryRank:#,0}"; + } + + private void showHistoryRankTexts(int dayIndex) + { + rankText.Text = ranks[dayIndex] > 0 ? $"#{ranks[dayIndex]:#,0}" : "no rank"; + relativeText.Text = dayIndex == ranks.Length ? "Now" : $"{ranks.Length - dayIndex} days ago"; + //plural should be handled in a general way + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + graph.Colour = colours.Yellow; + + if (user.Statistics.Rank > 0) + { + // use logarithmic coordinates + graph.Values = ranks.Select(x => -(float)Math.Log(x)); + graph.ResetBall(); + } + } + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + if ((invalidation & Invalidation.DrawSize) != 0) + { + graph.Height = DrawHeight - padding * 2 - primary_textsize - secondary_textsize * 2; + } + + return base.Invalidate(invalidation, source, shallPropagate); + } + + private class RankChartLineGraph : LineGraph + { + private readonly CircularContainer ball; + private bool ballShown; + + private const double transform_duration = 100; + + public Action BallMove; + public Action BallRelease; + + public RankChartLineGraph() + { + Add(ball = new CircularContainer + { + Size = new Vector2(8), + Masking = true, + Origin = Anchor.Centre, + Alpha = 0, + RelativePositionAxes = Axes.Both, + Children = new Drawable[] + { + new Box { RelativeSizeAxes = Axes.Both } + } + }); + } + + public void ResetBall() + { + ball.MoveTo(new Vector2(1, GetYPosition(Values.Last())), ballShown ? transform_duration : 0, EasingTypes.OutQuint); + ball.Show(); + BallRelease(); + ballShown = true; + } + + protected override bool OnMouseMove(InputState state) + { + if (ballShown) + { + var values = (IList)Values; + var position = ToLocalSpace(state.Mouse.NativeState.Position); + int count = Math.Max(values.Count, DefaultValueCount); + int index = (int)Math.Round(position.X / DrawWidth * (count - 1)); + if (index >= count - values.Count) + { + int i = index + values.Count - count; + float y = GetYPosition(values[i]); + if (Math.Abs(y * DrawHeight - position.Y) <= 8f) + { + ball.MoveTo(new Vector2(index / (float)(count - 1), y), transform_duration, EasingTypes.OutQuint); + BallMove(i); + } + } + } + return base.OnMouseMove(state); + } + + protected override void OnHoverLost(InputState state) + { + if (ballShown) + ResetBall(); + base.OnHoverLost(state); + } + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/AboutSection.cs b/osu.Game/Overlays/Profile/Sections/AboutSection.cs new file mode 100644 index 0000000000..1d15300eca --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/AboutSection.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Overlays.Profile.Sections +{ + public class AboutSection : ProfileSection + { + public override string Title => "me!"; + + public override string Identifier => "me"; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs new file mode 100644 index 0000000000..1c39223e6f --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Overlays.Profile.Sections +{ + public class BeatmapsSection : ProfileSection + { + public override string Title => "Beatmaps"; + + public override string Identifier => "beatmaps"; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs new file mode 100644 index 0000000000..78ed6bf846 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Overlays.Profile.Sections +{ + public class HistoricalSection : ProfileSection + { + public override string Title => "Historical"; + + public override string Identifier => "historical"; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs new file mode 100644 index 0000000000..3c36368fd7 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Overlays.Profile.Sections +{ + public class KudosuSection : ProfileSection + { + public override string Title => "Kudosu!"; + + public override string Identifier => "kudosu"; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/MedalsSection.cs b/osu.Game/Overlays/Profile/Sections/MedalsSection.cs new file mode 100644 index 0000000000..47a3e6f4d0 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/MedalsSection.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Overlays.Profile.Sections +{ + public class MedalsSection : ProfileSection + { + public override string Title => "Medals"; + + public override string Identifier => "medals"; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs new file mode 100644 index 0000000000..5ea135fcac --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Overlays.Profile.Sections +{ + public class RanksSection : ProfileSection + { + public override string Title => "Ranks"; + + public override string Identifier => "top_ranks"; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/RecentSection.cs b/osu.Game/Overlays/Profile/Sections/RecentSection.cs new file mode 100644 index 0000000000..7bd41eac79 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/RecentSection.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Overlays.Profile.Sections +{ + public class RecentSection : ProfileSection + { + public override string Title => "Recent"; + + public override string Identifier => "recent_activities"; + } +} diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 43d89eebf0..bbecdff29a 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 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; @@ -31,6 +32,11 @@ namespace osu.Game.Overlays.Settings.Sections.General private UserPanel panel; private UserDropdown dropdown; + /// + /// Called to request a hide of a parent displaying this container. + /// + public Action RequestHide; + public override RectangleF BoundingBox => bounding ? base.BoundingBox : RectangleF.Empty; public bool Bounding @@ -58,6 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { this.inputManager = inputManager; this.colours = colours; + api?.Register(this); } @@ -129,7 +136,11 @@ namespace osu.Game.Overlays.Settings.Sections.General }, }, }, - panel = new UserPanel(api.LocalUser.Value) { RelativeSizeAxes = Axes.X }, + panel = new UserPanel(api.LocalUser.Value) + { + RelativeSizeAxes = Axes.X, + Action = RequestHide + }, dropdown = new UserDropdown { RelativeSizeAxes = Axes.X }, }, }, diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 66b15234c3..86e1c1069c 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -86,7 +86,7 @@ namespace osu.Game.Overlays }, Exit = Hide, }, - Sections = sections, + Children = sections, Footer = new SettingsFooter() }, sidebar = new Sidebar @@ -163,13 +163,12 @@ namespace osu.Game.Overlays sectionsContainer.Padding = new MarginPadding { Top = getToolbarHeight() }; } - private class SettingsSectionsContainer : SectionsContainer + private class SettingsSectionsContainer : SectionsContainer { - public SearchContainer SearchContainer; - private readonly Box headerBackground; + public SearchContainer SearchContainer; - protected override Container CreateScrollContentContainer() - => SearchContainer = new SearchContainer + protected override FlowContainer CreateScrollContentContainer() + => SearchContainer = new SearchContainer { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, @@ -178,12 +177,11 @@ namespace osu.Game.Overlays public SettingsSectionsContainer() { - ScrollContainer.ScrollbarVisible = false; - Add(headerBackground = new Box + HeaderBackground = new Box { Colour = Color4.Black, - RelativeSizeAxes = Axes.X - }); + RelativeSizeAxes = Axes.Both + }; } protected override void UpdateAfterChildren() @@ -191,9 +189,7 @@ namespace osu.Game.Overlays base.UpdateAfterChildren(); // no null check because the usage of this class is strict - headerBackground.Height = ExpandableHeader.LayoutSize.Y + FixedHeader.LayoutSize.Y; - headerBackground.Y = ExpandableHeader.Y; - headerBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y * 0.5f; + HeaderBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y * 0.5f; } } } diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs new file mode 100644 index 0000000000..f26b143088 --- /dev/null +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -0,0 +1,224 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Profile; +using osu.Game.Overlays.Profile.Sections; +using osu.Game.Users; + +namespace osu.Game.Overlays +{ + public class UserProfileOverlay : WaveOverlayContainer + { + private ProfileSection lastSection; + private ProfileSection[] sections; + private GetUserRequest userReq; + private APIAccess api; + private ProfileHeader header; + private SectionsContainer sectionsContainer; + private ProfileTabControl tabs; + + public const float CONTENT_X_MARGIN = 50; + + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; + + protected override bool OnClick(InputState state) + { + State = Visibility.Hidden; + return true; + } + + public UserProfileOverlay() + { + FirstWaveColour = OsuColour.Gray(0.4f); + SecondWaveColour = OsuColour.Gray(0.3f); + ThirdWaveColour = OsuColour.Gray(0.2f); + FourthWaveColour = OsuColour.Gray(0.1f); + + RelativeSizeAxes = Axes.Both; + RelativePositionAxes = Axes.Both; + Width = 0.85f; + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + + Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0), + Type = EdgeEffectType.Shadow, + Radius = 10 + }; + } + + [BackgroundDependencyLoader] + private void load(APIAccess api) + { + this.api = api; + } + + protected override void PopIn() + { + base.PopIn(); + FadeEdgeEffectTo(0.5f, APPEAR_DURATION, EasingTypes.In); + } + + protected override void PopOut() + { + base.PopOut(); + FadeEdgeEffectTo(0, DISAPPEAR_DURATION, EasingTypes.Out); + } + + public void ShowUser(User user, bool fetchOnline = true) + { + userReq?.Cancel(); + Clear(); + lastSection = null; + + sections = new ProfileSection[] + { + new AboutSection(), + new RecentSection(), + new RanksSection(), + new MedalsSection(), + new HistoricalSection(), + new BeatmapsSection(), + new KudosuSection() + }; + tabs = new ProfileTabControl + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Height = 30 + }; + + Add(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(0.2f) + }); + + header = new ProfileHeader(user); + + Add(sectionsContainer = new SectionsContainer + { + RelativeSizeAxes = Axes.Both, + ExpandableHeader = header, + FixedHeader = tabs, + HeaderBackground = new Box + { + Colour = OsuColour.Gray(34), + RelativeSizeAxes = Axes.Both + } + }); + sectionsContainer.SelectedSection.ValueChanged += s => + { + if (lastSection != s) + { + lastSection = s; + tabs.Current.Value = lastSection; + } + }; + + tabs.Current.ValueChanged += s => + { + if (lastSection == null) + { + lastSection = sectionsContainer.Children.FirstOrDefault(); + if (lastSection != null) + tabs.Current.Value = lastSection; + return; + } + if (lastSection != s) + { + lastSection = s; + sectionsContainer.ScrollTo(lastSection); + } + }; + + if (fetchOnline) + { + userReq = new GetUserRequest(user.Id); + userReq.Success += userLoadComplete; + api.Queue(userReq); + } + else + { + userReq = null; + userLoadComplete(user); + } + + Show(); + } + + private void userLoadComplete(User user) + { + header.User = user; + + for (int i = 0; i < user.ProfileOrder.Length; i++) + { + string id = user.ProfileOrder[i]; + var sec = sections.FirstOrDefault(s => s.Identifier == id); + if (sec != null) + { + sec.Depth = -i; + sectionsContainer.Add(sec); + tabs.AddItem(sec); + } + } + } + + private class ProfileTabControl : PageTabControl + { + private readonly Box bottom; + + public ProfileTabControl() + { + TabContainer.RelativeSizeAxes &= ~Axes.X; + TabContainer.AutoSizeAxes |= Axes.X; + TabContainer.Anchor |= Anchor.x1; + TabContainer.Origin |= Anchor.x1; + Add(bottom = new Box + { + RelativeSizeAxes = Axes.X, + Height = 1, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + EdgeSmoothness = new Vector2(1) + }); + } + + protected override TabItem CreateTabItem(ProfileSection value) => new ProfileTabItem(value); + + protected override Dropdown CreateDropdown() => null; + + private class ProfileTabItem : PageTabItem + { + public ProfileTabItem(ProfileSection value) : base(value) + { + Text.Text = value.Title; + } + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + bottom.Colour = colours.Yellow; + } + } + } +} diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 8c5ab29736..37b426ac2c 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using Newtonsoft.Json; using osu.Framework.Configuration; @@ -11,17 +12,20 @@ namespace osu.Game.Users [JsonProperty(@"id")] public long Id = 1; + [JsonProperty(@"join_date")] + public DateTimeOffset JoinDate; + [JsonProperty(@"username")] public string Username; - [JsonProperty(@"country_code")] - public string CountryCode; - [JsonProperty(@"country")] public Country Country; public Bindable Status = new Bindable(); + [JsonProperty(@"age")] + public int? Age; + public int GlobalRank; public int CountryRank; @@ -51,5 +55,101 @@ namespace osu.Game.Users [JsonProperty(@"id")] public int? Id; } + + [JsonProperty(@"isAdmin")] + public bool IsAdmin; + + [JsonProperty(@"isSupporter")] + public bool IsSupporter; + + [JsonProperty(@"isGMT")] + public bool IsGMT; + + [JsonProperty(@"isQAT")] + public bool IsQAT; + + [JsonProperty(@"isBNG")] + public bool IsBNG; + + [JsonProperty(@"is_active")] + public bool Active; + + [JsonProperty(@"interests")] + public string Intrerests; + + [JsonProperty(@"occupation")] + public string Occupation; + + [JsonProperty(@"title")] + public string Title; + + [JsonProperty(@"location")] + public string Location; + + [JsonProperty(@"lastvisit")] + public DateTimeOffset LastVisit; + + [JsonProperty(@"twitter")] + public string Twitter; + + [JsonProperty(@"lastfm")] + public string Lastfm; + + [JsonProperty(@"skype")] + public string Skype; + + [JsonProperty(@"website")] + public string Website; + + [JsonProperty(@"playstyle")] + public string[] PlayStyle; + + [JsonProperty(@"playmode")] + public string PlayMode; + + [JsonProperty(@"profileOrder")] + public string[] ProfileOrder; + + [JsonProperty(@"kudosu")] + public KudosuCount Kudosu; + + public class KudosuCount + { + [JsonProperty(@"total")] + public int Total; + + [JsonProperty(@"available")] + public int Available; + } + + [JsonProperty(@"defaultStatistics")] + public UserStatistics Statistics; + + public class RankHistories + { + [JsonProperty(@"osu")] + public RankHistory Osu; + + [JsonProperty(@"taiko")] + public RankHistory Taiko; + + [JsonProperty(@"fruits")] + public RankHistory Fruits; + + [JsonProperty(@"mania")] + public RankHistory Mania; + } + + public class RankHistory + { + [JsonProperty(@"mode")] + public string Mode; + + [JsonProperty(@"data")] + public int[] Data; + } + + [JsonProperty(@"allRankHistories")] + public RankHistories AllRankHistories; } } diff --git a/osu.Game/Users/UserCoverBackground.cs b/osu.Game/Users/UserCoverBackground.cs new file mode 100644 index 0000000000..c0f0d09d9d --- /dev/null +++ b/osu.Game/Users/UserCoverBackground.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Users +{ + public class UserCoverBackground : Sprite + { + private readonly User user; + + public UserCoverBackground(User user) + { + this.user = user; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + if (!string.IsNullOrEmpty(user.CoverUrl)) + Texture = textures.Get(user.CoverUrl); + } + } +} diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 881aaf2e07..0780c2ffd0 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; @@ -9,29 +10,31 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; namespace osu.Game.Users { - public class UserPanel : Container + public class UserPanel : ClickableContainer { + private readonly User user; private const float height = 100; private const float content_padding = 10; private const float status_height = 30; - private OsuColour colours; - private readonly Container statusBar; private readonly Box statusBg; private readonly OsuSpriteText statusMessage; public readonly Bindable Status = new Bindable(); + public new Action Action; + public UserPanel(User user) { + this.user = user; + Height = height - status_height; Masking = true; CornerRadius = 5; @@ -44,7 +47,7 @@ namespace osu.Game.Users Children = new Drawable[] { - new AsyncLoadWrapper(new CoverBackgroundSprite(user) + new AsyncLoadWrapper(new UserCoverBackground(user) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -162,11 +165,17 @@ namespace osu.Game.Users }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(permitNulls: true)] + private void load(OsuColour colours, UserProfileOverlay profile) { - this.colours = colours; Status.ValueChanged += displayStatus; + Status.ValueChanged += status => statusBg.FadeColour(status?.GetAppropriateColour(colours) ?? colours.Gray5, 500, EasingTypes.OutQuint); + + base.Action = () => + { + Action?.Invoke(); + profile?.ShowUser(user); + }; } protected override void LoadComplete() @@ -191,26 +200,8 @@ namespace osu.Game.Users statusBar.FadeIn(transition_duration, EasingTypes.OutQuint); ResizeHeightTo(height, transition_duration, EasingTypes.OutQuint); - statusBg.FadeColour(status.GetAppropriateColour(colours), 500, EasingTypes.OutQuint); statusMessage.Text = status.Message; } } - - private class CoverBackgroundSprite : Sprite - { - private readonly User user; - - public CoverBackgroundSprite(User user) - { - this.user = user; - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - if (!string.IsNullOrEmpty(user.CoverUrl)) - Texture = textures.Get(user.CoverUrl); - } - } } } diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs new file mode 100644 index 0000000000..05f3d65f30 --- /dev/null +++ b/osu.Game/Users/UserStatistics.cs @@ -0,0 +1,64 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; + +namespace osu.Game.Users +{ + public class UserStatistics + { + [JsonProperty(@"level")] + public LevelInfo Level; + + public struct LevelInfo + { + [JsonProperty(@"current")] + public int Current; + + [JsonProperty(@"progress")] + public int Progress; + } + + [JsonProperty(@"pp")] + public decimal? PP; + + [JsonProperty(@"pp_rank")] + public int Rank; + + [JsonProperty(@"ranked_score")] + public long RankedScore; + + [JsonProperty(@"hit_accuracy")] + public decimal Accuracy; + + [JsonProperty(@"play_count")] + public int PlayCount; + + [JsonProperty(@"total_score")] + public long TotalScore; + + [JsonProperty(@"total_hits")] + public int TotalHits; + + [JsonProperty(@"maximum_combo")] + public int MaxCombo; + + [JsonProperty(@"replays_watched_by_others")] + public int ReplayWatched; + + [JsonProperty(@"grade_counts")] + public Grades GradesCount; + + public struct Grades + { + [JsonProperty(@"ss")] + public int SS; + + [JsonProperty(@"s")] + public int S; + + [JsonProperty(@"a")] + public int A; + } + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4f8d77367a..55b0ee3348 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -80,8 +80,10 @@ + + @@ -98,6 +100,18 @@ + + + + + + + + + + + + @@ -466,6 +480,7 @@ + @@ -516,11 +531,6 @@ - - - - -