diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs index 0af0ff5604..a32f0a13b8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs @@ -31,10 +31,8 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestMaximumDistanceTrackingWithoutMovement( - [Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)] - float circleSize, - [Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)] - double velocity) + [Values(0, 5, 10)] float circleSize, + [Values(0, 5, 10)] double velocity) { const double time_slider_start = 1000; diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs index 291ccd634d..3d8781d902 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -11,7 +9,10 @@ using osu.Game.Overlays; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -27,15 +28,20 @@ namespace osu.Game.Tests.Visual.Online private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; - private CommentsContainer commentsContainer; + private CommentsContainer commentsContainer = null!; + + private TextBox editorTextBox = null!; [SetUp] public void SetUp() => Schedule(() => + { Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, Child = commentsContainer = new CommentsContainer() - }); + }; + editorTextBox = commentsContainer.ChildrenOfType().First(); + }); [Test] public void TestIdleState() @@ -126,6 +132,44 @@ namespace osu.Game.Tests.Visual.Online commentsContainer.ChildrenOfType().Count(d => d.Comment.Pinned == withPinned) == 1); } + [Test] + public void TestPost() + { + setUpCommentsResponse(new CommentBundle { Comments = new List() }); + AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); + AddAssert("no comments placeholder shown", () => commentsContainer.ChildrenOfType().Any()); + + setUpPostResponse(); + AddStep("enter text", () => editorTextBox.Current.Value = "comm"); + AddStep("submit", () => commentsContainer.ChildrenOfType().First().TriggerClick()); + + AddUntilStep("comment sent", () => + { + string writtenText = editorTextBox.Current.Value; + var comment = commentsContainer.ChildrenOfType().LastOrDefault(); + return comment != null && comment.ChildrenOfType().Any(y => y.Text == writtenText); + }); + AddAssert("no comments placeholder removed", () => !commentsContainer.ChildrenOfType().Any()); + } + + [Test] + public void TestPostWithExistingComments() + { + setUpCommentsResponse(getExampleComments()); + AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); + + setUpPostResponse(); + AddStep("enter text", () => editorTextBox.Current.Value = "comm"); + AddStep("submit", () => commentsContainer.ChildrenOfType().Single().ChildrenOfType().First().TriggerClick()); + + AddUntilStep("comment sent", () => + { + string writtenText = editorTextBox.Current.Value; + var comment = commentsContainer.ChildrenOfType().LastOrDefault(); + return comment != null && comment.ChildrenOfType().Any(y => y.Text == writtenText); + }); + } + private void setUpCommentsResponse(CommentBundle commentBundle) => AddStep("set up response", () => { @@ -139,7 +183,33 @@ namespace osu.Game.Tests.Visual.Online }; }); - private CommentBundle getExampleComments(bool withPinned = false) + private void setUpPostResponse() + => AddStep("set up response", () => + { + dummyAPI.HandleRequest = request => + { + if (!(request is CommentPostRequest req)) + return false; + + req.TriggerSuccess(new CommentBundle + { + Comments = new List + { + new Comment + { + Id = 98, + Message = req.Message, + LegacyName = "FirstUser", + CreatedAt = DateTimeOffset.Now, + VotesCount = 98, + } + } + }); + return true; + }; + }); + + private static CommentBundle getExampleComments(bool withPinned = false) { var bundle = new CommentBundle { diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs index b86513eb49..ceddd4d1a1 100644 --- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs @@ -113,7 +113,7 @@ namespace osu.Game.Tournament.Screens.Setup new LabelledDropdown { Label = "Ruleset", - Description = "Decides what stats are displayed and which ranks are retrieved for players.", + Description = "Decides what stats are displayed and which ranks are retrieved for players. This requires a restart to reload data for an existing bracket.", Items = rulesets.AvailableRulesets, Current = LadderInfo.Ruleset, }, diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 08f21cb556..1d52fe305a 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -16,6 +16,7 @@ using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Graphics; +using osu.Game.Online; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Tournament.IO; @@ -44,6 +45,14 @@ namespace osu.Game.Tournament return dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); } + public override EndpointConfiguration CreateEndpoints() + { + if (UseDevelopmentServer) + return base.CreateEndpoints(); + + return new ProductionEndpointConfiguration(); + } + private TournamentSpriteText initialisationText; [BackgroundDependencyLoader] @@ -156,9 +165,21 @@ namespace osu.Game.Tournament addedInfo |= addSeedingBeatmaps(); if (addedInfo) - SaveChanges(); + saveChanges(); ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value); + + ladder.Ruleset.BindValueChanged(r => + { + // Refetch player rank data on next startup as the ruleset has changed. + foreach (var team in ladder.Teams) + { + foreach (var player in team.Players) + player.Rank = null; + } + + SaveChanges(); + }); } catch (Exception e) { @@ -306,6 +327,11 @@ namespace osu.Game.Tournament return; } + saveChanges(); + } + + private void saveChanges() + { foreach (var r in ladder.Rounds) r.Matches = ladder.Matches.Where(p => p.Round.Value == r).Select(p => p.ID).ToList(); diff --git a/osu.Game/Localisation/ChatStrings.cs b/osu.Game/Localisation/ChatStrings.cs index 7bd284a94e..6b0a6bd8e1 100644 --- a/osu.Game/Localisation/ChatStrings.cs +++ b/osu.Game/Localisation/ChatStrings.cs @@ -19,6 +19,11 @@ namespace osu.Game.Localisation /// public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"join the real-time discussion"); + /// + /// "Mention" + /// + public static LocalisableString MentionUser => new TranslatableString(getKey(@"mention_user"), @"Mention"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Online/API/Requests/CommentPostRequest.cs b/osu.Game/Online/API/Requests/CommentPostRequest.cs new file mode 100644 index 0000000000..45e5eb1a94 --- /dev/null +++ b/osu.Game/Online/API/Requests/CommentPostRequest.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using osu.Framework.IO.Network; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class CommentPostRequest : APIRequest + { + public readonly CommentableType Commentable; + public readonly long CommentableId; + public readonly string Message; + public readonly long? ParentCommentId; + + public CommentPostRequest(CommentableType commentable, long commentableId, string message, long? parentCommentId = null) + { + Commentable = commentable; + CommentableId = commentableId; + Message = message; + ParentCommentId = parentCommentId; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + + req.AddParameter(@"comment[commentable_type]", Commentable.ToString().ToLowerInvariant()); + req.AddParameter(@"comment[commentable_id]", $"{CommentableId}"); + req.AddParameter(@"comment[message]", Message); + if (ParentCommentId.HasValue) + req.AddParameter(@"comment[parent_id]", $"{ParentCommentId}"); + + return req; + } + + protected override string Target => "comments"; + } +} diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 0a5434822b..e3b5037367 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -25,6 +25,7 @@ namespace osu.Game.Online.Chat /// public partial class StandAloneChatDisplay : CompositeDrawable { + [Cached] public readonly Bindable Channel = new Bindable(); protected readonly ChatTextBox TextBox; diff --git a/osu.Game/Online/ExperimentalEndpointConfiguration.cs b/osu.Game/Online/ExperimentalEndpointConfiguration.cs new file mode 100644 index 0000000000..c3d0014c8b --- /dev/null +++ b/osu.Game/Online/ExperimentalEndpointConfiguration.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online +{ + public class ExperimentalEndpointConfiguration : EndpointConfiguration + { + public ExperimentalEndpointConfiguration() + { + WebsiteRootUrl = @"https://osu.ppy.sh"; + APIEndpointUrl = @"https://lazer.ppy.sh"; + APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; + APIClientID = "5"; + SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; + MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; + MetadataEndpointUrl = "https://spectator.ppy.sh/metadata"; + } + } +} diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs index 003ec50afd..0244761b65 100644 --- a/osu.Game/Online/ProductionEndpointConfiguration.cs +++ b/osu.Game/Online/ProductionEndpointConfiguration.cs @@ -1,16 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Online { public class ProductionEndpointConfiguration : EndpointConfiguration { public ProductionEndpointConfiguration() { - WebsiteRootUrl = @"https://osu.ppy.sh"; - APIEndpointUrl = @"https://lazer.ppy.sh"; + WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh"; APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; APIClientID = "5"; SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index b27be37591..1c50349941 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -98,8 +98,8 @@ namespace osu.Game public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild; - internal EndpointConfiguration CreateEndpoints() => - UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); + public virtual EndpointConfiguration CreateEndpoints() => + UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ExperimentalEndpointConfiguration(); public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); diff --git a/osu.Game/Overlays/Chat/DrawableUsername.cs b/osu.Game/Overlays/Chat/DrawableUsername.cs index 8cd16047f3..031a0b6ae2 100644 --- a/osu.Game/Overlays/Chat/DrawableUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableUsername.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -24,6 +25,7 @@ using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; +using ChatStrings = osu.Game.Localisation.ChatStrings; namespace osu.Game.Overlays.Chat { @@ -65,6 +67,9 @@ namespace osu.Game.Overlays.Chat [Resolved(canBeNull: true)] private UserProfileOverlay? profileOverlay { get; set; } + [Resolved] + private Bindable? currentChannel { get; set; } + private readonly APIUser user; private readonly OsuSpriteText drawableText; @@ -156,6 +161,14 @@ namespace osu.Game.Overlays.Chat if (!user.Equals(api.LocalUser.Value)) items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel)); + if (currentChannel?.Value != null) + { + items.Add(new OsuMenuItem(ChatStrings.MentionUser, MenuItemType.Standard, () => + { + currentChannel.Value.TextBoxMessage.Value += $"@{user.Username} "; + })); + } + return items.ToArray(); } } diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index 7bd2d6a5e6..a334fac215 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Online.API; @@ -17,16 +18,22 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Threading; using System.Collections.Generic; using JetBrains.Annotations; +using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Game.Graphics.Sprites; using osu.Game.Resources.Localisation.Web; -using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; +using osu.Game.Users.Drawables; +using osuTK; namespace osu.Game.Overlays.Comments { + [Cached] public partial class CommentsContainer : CompositeDrawable { private readonly Bindable type = new Bindable(); private readonly BindableLong id = new BindableLong(); + public IBindable Type => type; + public IBindable Id => id; public readonly Bindable Sort = new Bindable(); public readonly BindableBool ShowDeleted = new BindableBool(); @@ -46,12 +53,14 @@ namespace osu.Game.Overlays.Comments private DeletedCommentsCounter deletedCommentsCounter; private CommentsShowMoreButton moreButton; private TotalCommentsCounter commentCounter; + private UpdateableAvatar avatar; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + AddRangeInternal(new Drawable[] { new Box @@ -86,6 +95,32 @@ namespace osu.Game.Overlays.Comments }, }, }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 50, Vertical = 20 }, + Children = new Drawable[] + { + avatar = new UpdateableAvatar(api.LocalUser.Value) + { + Size = new Vector2(50), + CornerExponent = 2, + CornerRadius = 25, + Masking = true, + }, + new Container + { + Padding = new MarginPadding { Left = 60 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new NewCommentEditor + { + OnPost = prependPostedComments + } + } + } + }, new CommentsHeader { Sort = { BindTarget = Sort }, @@ -151,6 +186,7 @@ namespace osu.Game.Overlays.Comments protected override void LoadComplete() { User.BindValueChanged(_ => refetchComments()); + User.BindValueChanged(e => avatar.User = e.NewValue); Sort.BindValueChanged(_ => refetchComments(), true); base.LoadComplete(); } @@ -245,7 +281,6 @@ namespace osu.Game.Overlays.Comments { pinnedContent.AddRange(loaded.Where(d => d.Comment.Pinned)); content.AddRange(loaded.Where(d => !d.Comment.Pinned)); - deletedCommentsCounter.Count.Value += topLevelComments.Select(d => d.Comment).Count(c => c.IsDeleted && c.IsTopLevel); if (bundle.HasMore) @@ -288,6 +323,34 @@ namespace osu.Game.Overlays.Comments } } + private void prependPostedComments(CommentBundle bundle) + { + var topLevelComments = new List(); + + foreach (var comment in bundle.Comments) + { + // Exclude possible duplicated comments. + if (CommentDictionary.ContainsKey(comment.Id)) + continue; + + topLevelComments.Add(getDrawableComment(comment)); + } + + if (topLevelComments.Any()) + { + LoadComponentsAsync(topLevelComments, loaded => + { + if (content.Count > 0 && content[0] is NoCommentsPlaceholder placeholder) + content.Remove(placeholder, true); + + foreach (var comment in loaded) + { + content.Insert((int)-Clock.CurrentTime, comment); + } + }, (loadCancellation = new CancellationTokenSource()).Token); + } + } + private DrawableComment getDrawableComment(Comment comment) { if (CommentDictionary.TryGetValue(comment.Id, out var existing)) @@ -317,7 +380,7 @@ namespace osu.Game.Overlays.Comments base.Dispose(isDisposing); } - private partial class NoCommentsPlaceholder : CompositeDrawable + internal partial class NoCommentsPlaceholder : CompositeDrawable { [BackgroundDependencyLoader] private void load() @@ -336,5 +399,41 @@ namespace osu.Game.Overlays.Comments }); } } + + private partial class NewCommentEditor : CommentEditor + { + [Resolved] + private CommentsContainer commentsContainer { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } + + public Action OnPost; + + //TODO should match web, left empty due to no multiline support + protected override LocalisableString FooterText => default; + + protected override LocalisableString CommitButtonText => CommonStrings.ButtonsPost; + + protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderNew; + + protected override void OnCommit(string text) + { + ShowLoadingSpinner = true; + CommentPostRequest req = new CommentPostRequest(commentsContainer.Type.Value, commentsContainer.Id.Value, text); + req.Failure += e => Schedule(() => + { + ShowLoadingSpinner = false; + Logger.Error(e, "Posting comment failed."); + }); + req.Success += cb => Schedule(() => + { + ShowLoadingSpinner = false; + Current.Value = string.Empty; + OnPost?.Invoke(cb); + }); + api.Queue(req); + } + } } } diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 834abd5e9f..1df65ab4b1 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -57,6 +57,11 @@ namespace osu.Game.Overlays.Comments /// public bool WasDeleted { get; protected set; } + /// + /// Tracks this comment's level of nesting. 0 means that this comment has no parents. + /// + public int Level { get; private set; } + private FillFlowContainer childCommentsVisibilityContainer = null!; private FillFlowContainer childCommentsContainer = null!; private LoadRepliesButton loadRepliesButton = null!; @@ -70,7 +75,7 @@ namespace osu.Game.Overlays.Comments private GridContainer content = null!; private VotePill votePill = null!; - [Resolved(canBeNull: true)] + [Resolved] private IDialogOverlay? dialogOverlay { get; set; } [Resolved] @@ -79,7 +84,7 @@ namespace osu.Game.Overlays.Comments [Resolved] private GameHost host { get; set; } = null!; - [Resolved(canBeNull: true)] + [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } public DrawableComment(Comment comment) @@ -88,12 +93,16 @@ namespace osu.Game.Overlays.Comments } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, DrawableComment? parentComment) { LinkFlowContainer username; FillFlowContainer info; CommentMarkdownContainer message; + Level = parentComment?.Level + 1 ?? 0; + + float childrenPadding = Level < 6 ? 20 : 5; + RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; InternalChildren = new Drawable[] @@ -248,7 +257,7 @@ namespace osu.Game.Overlays.Comments RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Padding = new MarginPadding { Left = 20 }, + Padding = new MarginPadding { Left = childrenPadding }, Children = new Drawable[] { childCommentsContainer = new FillFlowContainer diff --git a/osu.Game/Overlays/Profile/Header/Components/ExtendedDetails.cs b/osu.Game/Overlays/Profile/Header/Components/ExtendedDetails.cs new file mode 100644 index 0000000000..50fc52600c --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/ExtendedDetails.cs @@ -0,0 +1,110 @@ +// 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.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public partial class ExtendedDetails : CompositeDrawable + { + public Bindable User { get; } = new Bindable(); + + private SpriteText rankedScore = null!; + private SpriteText hitAccuracy = null!; + private SpriteText playCount = null!; + private SpriteText totalScore = null!; + private SpriteText totalHits = null!; + private SpriteText maximumCombo = null!; + private SpriteText replaysWatched = null!; + + [BackgroundDependencyLoader] + private void load() + { + var font = OsuFont.Default.With(size: 12); + const float vertical_spacing = 4; + + AutoSizeAxes = Axes.Both; + + // this should really be a grid, but trying to avoid one to avoid the performance hit. + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20, 0), + Children = new[] + { + new FillFlowContainer + { + Name = @"Labels", + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, vertical_spacing), + Children = new Drawable[] + { + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsRankedScore }, + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsHitAccuracy }, + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsPlayCount }, + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsTotalScore }, + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsTotalHits }, + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsMaximumCombo }, + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsReplaysWatchedByOthers }, + } + }, + new FillFlowContainer + { + Name = @"Values", + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, vertical_spacing), + Children = new Drawable[] + { + rankedScore = new OsuSpriteText { Font = font }, + hitAccuracy = new OsuSpriteText { Font = font }, + playCount = new OsuSpriteText { Font = font }, + totalScore = new OsuSpriteText { Font = font }, + totalHits = new OsuSpriteText { Font = font }, + maximumCombo = new OsuSpriteText { Font = font }, + replaysWatched = new OsuSpriteText { Font = font }, + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + User.BindValueChanged(user => updateStatistics(user.NewValue?.User.Statistics), true); + } + + private void updateStatistics(UserStatistics? statistics) + { + if (statistics == null) + { + Alpha = 0; + return; + } + + Alpha = 1; + + rankedScore.Text = statistics.RankedScore.ToLocalisableString(@"N0"); + hitAccuracy.Text = statistics.DisplayAccuracy; + playCount.Text = statistics.PlayCount.ToLocalisableString(@"N0"); + totalScore.Text = statistics.TotalScore.ToLocalisableString(@"N0"); + totalHits.Text = statistics.TotalHits.ToLocalisableString(@"N0"); + maximumCombo.Text = statistics.MaxCombo.ToLocalisableString(@"N0"); + replaysWatched.Text = statistics.ReplaysWatched.ToLocalisableString(@"N0"); + } + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs new file mode 100644 index 0000000000..b89973c5e5 --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -0,0 +1,186 @@ +// 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.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Leaderboards; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public partial class MainDetails : CompositeDrawable + { + private readonly Dictionary scoreRankInfos = new Dictionary(); + private ProfileValueDisplay medalInfo = null!; + private ProfileValueDisplay ppInfo = null!; + private ProfileValueDisplay detailGlobalRank = null!; + private ProfileValueDisplay detailCountryRank = null!; + private RankGraph rankGraph = null!; + + public readonly Bindable User = new Bindable(); + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + AutoSizeDuration = 200, + AutoSizeEasing = Easing.OutQuint, + Masking = true, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 15), + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20), + Children = new Drawable[] + { + detailGlobalRank = new ProfileValueDisplay(true) + { + Title = UsersStrings.ShowRankGlobalSimple, + }, + detailCountryRank = new ProfileValueDisplay(true) + { + Title = UsersStrings.ShowRankCountrySimple, + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 60, + Children = new Drawable[] + { + rankGraph = new RankGraph + { + RelativeSizeAxes = Axes.Both, + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + medalInfo = new ProfileValueDisplay + { + Title = UsersStrings.ShowStatsMedals, + }, + ppInfo = new ProfileValueDisplay + { + Title = "pp", + }, + new TotalPlayTime + { + User = { BindTarget = User } + }, + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new[] + { + scoreRankInfos[ScoreRank.XH] = new ScoreRankInfo(ScoreRank.XH), + scoreRankInfos[ScoreRank.X] = new ScoreRankInfo(ScoreRank.X), + scoreRankInfos[ScoreRank.SH] = new ScoreRankInfo(ScoreRank.SH), + scoreRankInfos[ScoreRank.S] = new ScoreRankInfo(ScoreRank.S), + scoreRankInfos[ScoreRank.A] = new ScoreRankInfo(ScoreRank.A), + } + } + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + User.BindValueChanged(e => updateDisplay(e.NewValue), true); + } + + private void updateDisplay(UserProfileData? data) + { + var user = data?.User; + + medalInfo.Content = user?.Achievements?.Length.ToString() ?? "0"; + ppInfo.Content = user?.Statistics?.PP?.ToLocalisableString("#,##0") ?? (LocalisableString)"0"; + + foreach (var scoreRankInfo in scoreRankInfos) + scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0; + + detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; + detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; + + rankGraph.Statistics.Value = user?.Statistics; + } + + private partial class ScoreRankInfo : CompositeDrawable + { + private readonly OsuSpriteText rankCount; + + public int RankCount + { + set => rankCount.Text = value.ToLocalisableString("#,##0"); + } + + public ScoreRankInfo(ScoreRank rank) + { + AutoSizeAxes = Axes.Both; + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + Width = 44, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DrawableRank(rank) + { + RelativeSizeAxes = Axes.X, + Height = 22, + }, + rankCount = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + } + } + }; + } + } + } +} diff --git a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs index 890e6c4e04..1cc3aae735 100644 --- a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs @@ -1,33 +1,17 @@ // 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.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.Leaderboards; using osu.Game.Overlays.Profile.Header.Components; -using osu.Game.Resources.Localisation.Web; -using osu.Game.Scoring; -using osuTK; namespace osu.Game.Overlays.Profile.Header { public partial class DetailHeaderContainer : CompositeDrawable { - private readonly Dictionary scoreRankInfos = new Dictionary(); - private ProfileValueDisplay medalInfo = null!; - private ProfileValueDisplay ppInfo = null!; - private ProfileValueDisplay detailGlobalRank = null!; - private ProfileValueDisplay detailCountryRank = null!; - private RankGraph rankGraph = null!; - public readonly Bindable User = new Bindable(); [BackgroundDependencyLoader] @@ -35,8 +19,6 @@ namespace osu.Game.Overlays.Profile.Header { AutoSizeAxes = Axes.Y; - User.ValueChanged += e => updateDisplay(e.NewValue); - InternalChildren = new Drawable[] { new Box @@ -44,149 +26,52 @@ namespace osu.Game.Overlays.Profile.Header RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5, }, - new FillFlowContainer + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - AutoSizeDuration = 200, - AutoSizeEasing = Easing.OutQuint, - Masking = true, Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 }, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 15), - Children = new Drawable[] + Child = new GridContainer { - new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(20), - Children = new Drawable[] - { - detailGlobalRank = new ProfileValueDisplay(true) - { - Title = UsersStrings.ShowRankGlobalSimple, - }, - detailCountryRank = new ProfileValueDisplay(true) - { - Title = UsersStrings.ShowRankCountrySimple, - }, - } + new Dimension(GridSizeMode.AutoSize), }, - new Container + ColumnDimensions = new[] { - RelativeSizeAxes = Axes.X, - Height = 60, - Children = new Drawable[] - { - rankGraph = new RankGraph - { - RelativeSizeAxes = Axes.Both, - }, - } + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), }, - new Container + Content = new[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + new Drawable[] { - new FillFlowContainer + new MainDetails + { + RelativeSizeAxes = Axes.X, + User = { BindTarget = User } + }, + new Box + { + RelativeSizeAxes = Axes.Y, + Width = 2, + Colour = colourProvider.Background6, + Margin = new MarginPadding { Horizontal = 15 } + }, + new ExtendedDetails { - AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - medalInfo = new ProfileValueDisplay - { - Title = UsersStrings.ShowStatsMedals, - }, - ppInfo = new ProfileValueDisplay - { - Title = "pp", - }, - new TotalPlayTime - { - User = { BindTarget = User } - }, - } - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Children = new[] - { - scoreRankInfos[ScoreRank.XH] = new ScoreRankInfo(ScoreRank.XH), - scoreRankInfos[ScoreRank.X] = new ScoreRankInfo(ScoreRank.X), - scoreRankInfos[ScoreRank.SH] = new ScoreRankInfo(ScoreRank.SH), - scoreRankInfos[ScoreRank.S] = new ScoreRankInfo(ScoreRank.S), - scoreRankInfos[ScoreRank.A] = new ScoreRankInfo(ScoreRank.A), - } + User = { BindTarget = User } } } - }, - } - }, - }; - } - - private void updateDisplay(UserProfileData? data) - { - var user = data?.User; - - medalInfo.Content = user?.Achievements?.Length.ToString() ?? "0"; - ppInfo.Content = user?.Statistics?.PP?.ToLocalisableString("#,##0") ?? (LocalisableString)"0"; - - foreach (var scoreRankInfo in scoreRankInfos) - scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0; - - detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; - detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; - - rankGraph.Statistics.Value = user?.Statistics; - } - - private partial class ScoreRankInfo : CompositeDrawable - { - private readonly OsuSpriteText rankCount; - - public int RankCount - { - set => rankCount.Text = value.ToLocalisableString("#,##0"); - } - - public ScoreRankInfo(ScoreRank rank) - { - AutoSizeAxes = Axes.Both; - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - Width = 44, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new DrawableRank(rank) - { - RelativeSizeAxes = Axes.X, - Height = 22, - }, - rankCount = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre } } - }; - } + } + }; } } } diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index e04a8ad9ad..1fe39cb570 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -5,18 +5,15 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays.Profile.Header.Components; -using osu.Game.Resources.Localisation.Web; using osu.Game.Users.Drawables; using osuTK; @@ -38,7 +35,6 @@ namespace osu.Game.Overlays.Profile.Header private OsuSpriteText titleText = null!; private UpdateableFlag userFlag = null!; private OsuSpriteText userCountryText = null!; - private FillFlowContainer userStats = null!; private GroupBadgeFlow groupBadgeFlow = null!; [BackgroundDependencyLoader] @@ -164,16 +160,6 @@ namespace osu.Game.Overlays.Profile.Header } } }, - userStats = new FillFlowContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Y, - Width = 300, - Margin = new MarginPadding { Right = UserProfileOverlay.CONTENT_X_MARGIN }, - Padding = new MarginPadding { Vertical = 15 }, - Spacing = new Vector2(0, 2) - } }; User.BindValueChanged(user => updateUser(user.NewValue)); @@ -192,43 +178,6 @@ namespace osu.Game.Overlays.Profile.Header titleText.Text = user?.Title ?? string.Empty; titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); groupBadgeFlow.User.Value = user; - - userStats.Clear(); - - if (user?.Statistics != null) - { - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsRankedScore, user.Statistics.RankedScore.ToLocalisableString("#,##0"))); - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsHitAccuracy, user.Statistics.DisplayAccuracy)); - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsPlayCount, user.Statistics.PlayCount.ToLocalisableString("#,##0"))); - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsTotalScore, user.Statistics.TotalScore.ToLocalisableString("#,##0"))); - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsTotalHits, user.Statistics.TotalHits.ToLocalisableString("#,##0"))); - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsMaximumCombo, user.Statistics.MaxCombo.ToLocalisableString("#,##0"))); - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsReplaysWatchedByOthers, user.Statistics.ReplaysWatched.ToLocalisableString("#,##0"))); - } - } - - private partial class UserStatsLine : Container - { - public UserStatsLine(LocalisableString left, LocalisableString right) - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Children = new Drawable[] - { - new OsuSpriteText - { - Font = OsuFont.GetFont(size: 15), - Text = left, - }, - new OsuSpriteText - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold), - Text = right, - }, - }; - } } } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 19eced08d9..774ecc2c9c 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -596,6 +596,9 @@ namespace osu.Game.Screens.Select public void FlushPendingFilterOperations() { + if (!IsLoaded) + return; + if (PendingFilter?.Completed == false) { applyActiveCriteria(false); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 64deb59026..4b3222c14a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -863,6 +863,7 @@ namespace osu.Game.Screens.Select // if we have a pending filter operation, we want to run it now. // it could change selection (ie. if the ruleset has been changed). Carousel.FlushPendingFilterOperations(); + return true; }