diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileScoreSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileScoreSubsection.cs new file mode 100644 index 0000000000..408d4a96bf --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileScoreSubsection.cs @@ -0,0 +1,193 @@ +// 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; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +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.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Overlays.Profile.Sections +{ + public abstract class PaginatedProfileScoreSubsection : ProfileSubsection + { + /// + /// The number of items displayed per page. + /// + protected virtual int ItemsPerPage => 50; + + /// + /// The number of items displayed initially. + /// + protected virtual int InitialItemsCount => 5; + + [Resolved] + private IAPIProvider api { get; set; } + + protected PaginationParameters? CurrentPage { get; private set; } + + protected ReverseChildIDFillFlowContainer ItemsContainer { get; private set; } + + private APIRequest> scoreRetrievalRequest; + private APIRequest beatmapRetrievalRequest; + private CancellationTokenSource loadCancellation; + + protected List CurrentScores { get; private set; } = new List(); + + private ShowMoreButton moreButton; + private OsuSpriteText missing; + private readonly LocalisableString? missingText; + + protected PaginatedProfileScoreSubsection(Bindable user, LocalisableString? headerText = null, LocalisableString? missingText = null) + : base(user, headerText, CounterVisibilityState.AlwaysVisible) + { + this.missingText = missingText; + } + + protected override Drawable CreateContent() => new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + // reverse ID flow is required for correct Z-ordering of the items (last item should be front-most). + // particularly important in PaginatedBeatmapContainer, as it uses beatmap cards, which have expandable overhanging content. + ItemsContainer = new ReverseChildIDFillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Spacing = new Vector2(0, 2), + // ensure the container and its contents are in front of the "more" button. + Depth = float.MinValue + }, + moreButton = new ShowMoreButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Alpha = 0, + Margin = new MarginPadding { Top = 10 }, + Action = showMore, + }, + missing = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 15), + Text = missingText ?? string.Empty, + Alpha = 0, + } + } + }; + + protected override void LoadComplete() + { + base.LoadComplete(); + User.BindValueChanged(onUserChanged, true); + } + + private void onUserChanged(ValueChangedEvent e) + { + loadCancellation?.Cancel(); + scoreRetrievalRequest?.Cancel(); + beatmapRetrievalRequest?.Cancel(); + + CurrentPage = null; + ItemsContainer.Clear(); + CurrentScores.Clear(); + + if (e.NewValue != null) + { + showMore(); + SetCount(GetCount(e.NewValue)); + } + } + + private void showMore() + { + loadCancellation = new CancellationTokenSource(); + + CurrentPage = CurrentPage?.TakeNext(ItemsPerPage) ?? new PaginationParameters(InitialItemsCount); + + scoreRetrievalRequest = CreateScoreRequest(CurrentPage.Value); + scoreRetrievalRequest.Success += requestBeatmaps; + + api.Queue(scoreRetrievalRequest); + } + + private void requestBeatmaps(List items) + { + CurrentScores = items; + + beatmapRetrievalRequest = CreateBeatmapsRequest(items); + beatmapRetrievalRequest.Success += UpdateItems; + + api.Queue(beatmapRetrievalRequest); + } + + protected virtual APIRequest CreateBeatmapsRequest(List items) => new GetBeatmapsRequest(items.Select(i => i.BeatmapID).ToArray()); + + protected virtual void UpdateItems(GetBeatmapsResponse beatmaps) => Schedule(() => + { + var scoreBeatmapPairs = new List>(); + + foreach (var score in CurrentScores) + { + var beatmap = beatmaps.Beatmaps.Find(m => m.OnlineID == score.BeatmapID); + scoreBeatmapPairs.Add(new Tuple(score, beatmap)); + } + + OnItemsReceived(scoreBeatmapPairs); + + if (!scoreBeatmapPairs.Any() && CurrentPage?.Offset == 0) + { + moreButton.Hide(); + moreButton.IsLoading = false; + + if (missingText.HasValue) + missing.Show(); + + return; + } + + LoadComponentsAsync(scoreBeatmapPairs.Select(CreateDrawableItem).Where(d => d != null), drawables => + { + missing.Hide(); + + moreButton.FadeTo(scoreBeatmapPairs.Count == CurrentPage?.Limit ? 1 : 0); + moreButton.IsLoading = false; + + ItemsContainer.AddRange(drawables); + }, loadCancellation.Token); + }); + + protected virtual int GetCount(APIUser user) => 0; + + protected virtual void OnItemsReceived(List> scoreBeatmapPairs) + { + } + + protected abstract APIRequest> CreateScoreRequest(PaginationParameters pagination); + + protected abstract Drawable CreateDrawableItem(Tuple scoreBeatmapPair); + + protected override void Dispose(bool isDisposing) + { + scoreRetrievalRequest?.Cancel(); + loadCancellation?.Cancel(); + base.Dispose(isDisposing); + } + } +}