Squash commits from private fork

Temporary comments left to-remove later
This commit is contained in:
「空白」 2020-05-12 03:18:47 +09:00
parent d5ae8f5ef4
commit 35e7cee458
6 changed files with 236 additions and 45 deletions

View File

@ -13,4 +13,13 @@ namespace osu.Game.Online.API.Requests
[JsonProperty("cursor")] [JsonProperty("cursor")]
public dynamic CursorJson; public dynamic CursorJson;
} }
public abstract class ResponseWithCursor<T> : ResponseWithCursor where T : class
{
/// <summary>
/// Cursor deserialized into T class type (cannot implicitly convert type to object using raw Cursor)
/// </summary>
[JsonProperty("cursor")]
public T Cursor;
}
} }

View File

@ -5,11 +5,21 @@ using osu.Framework.IO.Network;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using Newtonsoft.Json;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.API.Requests
{ {
public class SearchBeatmapSetsRequest : APIRequest<SearchBeatmapSetsResponse> public class SearchBeatmapSetsRequest : APIRequest<SearchBeatmapSetsResponse>
{ {
public class Cursor
{
[JsonProperty("approved_date")]
public string ApprovedDate;
[JsonProperty("_id")]
public string Id;
}
public SearchCategory SearchCategory { get; set; } public SearchCategory SearchCategory { get; set; }
public SortCriteria SortCriteria { get; set; } public SortCriteria SortCriteria { get; set; }
@ -22,17 +32,20 @@ namespace osu.Game.Online.API.Requests
private readonly string query; private readonly string query;
private readonly RulesetInfo ruleset; private readonly RulesetInfo ruleset;
private readonly Cursor cursor;
private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc"; private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc";
public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset) public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, Cursor cursor = null,
SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending)
{ {
this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query);
this.ruleset = ruleset; this.ruleset = ruleset;
this.cursor = cursor;
SearchCategory = SearchCategory.Any; SearchCategory = searchCategory;
SortCriteria = SortCriteria.Ranked; SortCriteria = sortCriteria;
SortDirection = SortDirection.Descending; SortDirection = sortDirection;
Genre = SearchGenre.Any; Genre = SearchGenre.Any;
Language = SearchLanguage.Any; Language = SearchLanguage.Any;
} }
@ -55,6 +68,12 @@ namespace osu.Game.Online.API.Requests
req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}"); req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}");
if (cursor != null)
{
req.AddParameter("cursor[_id]", cursor.Id);
req.AddParameter("cursor[approved_date]", cursor.ApprovedDate);
}
return req; return req;
} }

View File

@ -7,7 +7,7 @@ using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.API.Requests
{ {
public class SearchBeatmapSetsResponse : ResponseWithCursor public class SearchBeatmapSetsResponse : ResponseWithCursor<SearchBeatmapSetsRequest.Cursor>
{ {
[JsonProperty("beatmapsets")] [JsonProperty("beatmapsets")]
public IEnumerable<APIBeatmapSet> BeatmapSets; public IEnumerable<APIBeatmapSet> BeatmapSets;

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -13,7 +12,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -24,6 +22,8 @@ namespace osu.Game.Overlays.BeatmapListing
{ {
public Action<List<BeatmapSetInfo>> SearchFinished; public Action<List<BeatmapSetInfo>> SearchFinished;
public Action SearchStarted; public Action SearchStarted;
/// <summary> List of currently displayed beatmap entries </summary>
private List<BeatmapSetInfo> currentBeatmaps;
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
@ -35,7 +35,7 @@ namespace osu.Game.Overlays.BeatmapListing
private readonly BeatmapListingSortTabControl sortControl; private readonly BeatmapListingSortTabControl sortControl;
private readonly Box sortControlBackground; private readonly Box sortControlBackground;
private SearchBeatmapSetsRequest getSetsRequest; private BeatmapListingPager beatmapListingPager;
public BeatmapListingFilterControl() public BeatmapListingFilterControl()
{ {
@ -115,12 +115,13 @@ namespace osu.Game.Overlays.BeatmapListing
} }
private ScheduledDelegate queryChangedDebounce; private ScheduledDelegate queryChangedDebounce;
private ScheduledDelegate queryPagingDebounce;
private void queueUpdateSearch(bool queryTextChanged = false) private void queueUpdateSearch(bool queryTextChanged = false)
{ {
SearchStarted?.Invoke(); SearchStarted?.Invoke();
getSetsRequest?.Cancel(); beatmapListingPager?.Reset();
queryChangedDebounce?.Cancel(); queryChangedDebounce?.Cancel();
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
@ -128,37 +129,55 @@ namespace osu.Game.Overlays.BeatmapListing
private void updateSearch() private void updateSearch()
{ {
getSetsRequest = new SearchBeatmapSetsRequest(searchControl.Query.Value, searchControl.Ruleset.Value) beatmapListingPager = new BeatmapListingPager(
{ api,
SearchCategory = searchControl.Category.Value, rulesets,
SortCriteria = sortControl.Current.Value, searchControl.Query.Value,
SortDirection = sortControl.SortDirection.Value, searchControl.Ruleset.Value,
Genre = searchControl.Genre.Value, searchControl.Category.Value,
Language = searchControl.Language.Value sortControl.Current.Value,
}; sortControl.SortDirection.Value
);
getSetsRequest.Success += response => Schedule(() => onSearchFinished(response)); queryPagingDebounce?.Cancel();
queryPagingDebounce = null;
beatmapListingPager.PageFetched += onSearchFinished;
api.Queue(getSetsRequest); AddPageToResult();
} }
private void onSearchFinished(SearchBeatmapSetsResponse response) private void onSearchFinished(List<BeatmapSetInfo> beatmaps)
{ {
var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList(); queryPagingDebounce = Scheduler.AddDelayed(() => queryPagingDebounce = null, 1000);
searchControl.BeatmapSet = response.Total == 0 ? null : beatmaps.First();
if (currentBeatmaps == null || !beatmapListingPager.IsPastFirstPage)
currentBeatmaps = beatmaps;
else
currentBeatmaps.AddRange(beatmaps);
SearchFinished?.Invoke(beatmaps); SearchFinished?.Invoke(beatmaps);
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
getSetsRequest?.Cancel(); beatmapListingPager?.Reset();
queryChangedDebounce?.Cancel(); queryChangedDebounce?.Cancel();
queryPagingDebounce?.Cancel();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
public void TakeFocus() => searchControl.TakeFocus(); public void TakeFocus() => searchControl.TakeFocus();
/// <summary> Request next 50 matches if available </summary>
public void AddPageToResult()
{
if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage)
return;
if (queryPagingDebounce != null)
return;
beatmapListingPager.FetchNextPage();
}
} }
} }

View File

@ -0,0 +1,92 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
namespace osu.Game.Overlays.BeatmapListing
{
public class BeatmapListingPager
{
private readonly IAPIProvider api;
private readonly RulesetStore rulesets;
private readonly string query;
private readonly RulesetInfo ruleset;
private readonly SearchCategory searchCategory;
private readonly SortCriteria sortCriteria;
private readonly SortDirection sortDirection;
public event PageFetchHandler PageFetched;
private SearchBeatmapSetsRequest getSetsRequest;
private SearchBeatmapSetsResponse lastResponse;
/// <summary> Reports end of results </summary>
private bool isLastPageFetched = false;
/// <summary> Job in process lock flag </summary>
private bool isFetching => getSetsRequest != null;
/// <summary> Whether beatmaps should be appended or replaced </summary>
public bool IsPastFirstPage { get; private set; } = false;
/// <summary> call FetchNextPage() safe-check </summary>
public bool CanFetchNextPage => !isLastPageFetched && !isFetching;
public BeatmapListingPager(IAPIProvider api, RulesetStore rulesets, string query, RulesetInfo ruleset, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending)
{
this.api = api;
this.rulesets = rulesets;
this.query = query;
this.ruleset = ruleset;
this.searchCategory = searchCategory;
this.sortCriteria = sortCriteria;
this.sortDirection = sortDirection;
}
public void FetchNextPage()
{
if (isFetching)
return;
if (lastResponse != null)
IsPastFirstPage = true;
getSetsRequest = new SearchBeatmapSetsRequest(
query,
ruleset,
lastResponse?.Cursor,
searchCategory,
sortCriteria,
sortDirection);
getSetsRequest.Success += response =>
{
var sets = response.BeatmapSets.Select(responseJson => responseJson.ToBeatmapSet(rulesets)).ToList();
if (sets.Count == 0)
isLastPageFetched = true;
lastResponse = response;
getSetsRequest = null;
PageFetched?.Invoke(sets);
};
api.Queue(getSetsRequest);
}
public void Reset()
{
isLastPageFetched = false;
IsPastFirstPage = false;
lastResponse = null;
getSetsRequest?.Cancel();
getSetsRequest = null;
}
public delegate void PageFetchHandler(List<BeatmapSetInfo> sets);
}
}

View File

@ -30,13 +30,21 @@ namespace osu.Game.Overlays
private Drawable currentContent; private Drawable currentContent;
private LoadingLayer loadingLayer; private LoadingLayer loadingLayer;
private Container panelTarget; private Container panelTarget;
private FillFlowContainer<BeatmapPanel> foundContent;
private NotFoundDrawable notFoundContent;
private OverlayScrollContainer resultScrollContainer;
/// <summary> Scroll distance threshold from results tail, higher means sooner </summary>
private const int pagination_scroll_distance = 500;
/// <summary> This is paging event flag </summary>
private bool shouldAddNextPage => resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance);
public BeatmapListingOverlay() public BeatmapListingOverlay()
: base(OverlayColourScheme.Blue) : base(OverlayColourScheme.Blue)
{ {
} }
private BeatmapListingFilterControl filterControl; private BeatmapListingFilterControl filterControl;//actual search settings
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -48,7 +56,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6 Colour = ColourProvider.Background6
}, },
new OverlayScrollContainer resultScrollContainer = new OverlayScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false, ScrollbarVisible = false,
@ -80,9 +88,14 @@ namespace osu.Game.Overlays
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Horizontal = 20 } Padding = new MarginPadding { Horizontal = 20 },
}, Children = new Drawable[]
loadingLayer = new LoadingLayer(panelTarget) {
foundContent = new FillFlowContainer<BeatmapPanel>(),
notFoundContent = new NotFoundDrawable(),
loadingLayer = new LoadingLayer(panelTarget)
}
}
} }
}, },
} }
@ -112,27 +125,52 @@ namespace osu.Game.Overlays
private void onSearchFinished(List<BeatmapSetInfo> beatmaps) private void onSearchFinished(List<BeatmapSetInfo> beatmaps)
{ {
//No matches case
if (!beatmaps.Any()) if (!beatmaps.Any())
{ {
LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
return; return;
} }
var newPanels = new FillFlowContainer<BeatmapPanel> //New query case
if (!shouldAddNextPage)
{ {
RelativeSizeAxes = Axes.X, //Spawn new child
AutoSizeAxes = Axes.Y, var newPanels = new FillFlowContainer<BeatmapPanel>
Spacing = new Vector2(10),
Alpha = 0,
Margin = new MarginPadding { Vertical = 15 },
ChildrenEnumerable = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
{ {
Anchor = Anchor.TopCentre, RelativeSizeAxes = Axes.X,
Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Y,
}) Spacing = new Vector2(10),
}; Alpha = 0,
Margin = new MarginPadding { Vertical = 15 },
ChildrenEnumerable = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
})
};
LoadComponentAsync(newPanels, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); foundContent = newPanels;
LoadComponentAsync(foundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
}
//Pagination case
else
{
beatmaps.ForEach(x =>
{
LoadComponentAsync(new GridBeatmapPanel(x)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
}, loaded =>
{
foundContent.Add(loaded);
loaded.FadeIn(200, Easing.OutQuint);
});
});
}
} }
private void addContentToPlaceholder(Drawable content) private void addContentToPlaceholder(Drawable content)
@ -149,13 +187,18 @@ namespace osu.Game.Overlays
// If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird. // If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird.
// At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0. // At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0.
// To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so. // To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so.
lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y); lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y)
.Then().Schedule(() => panelTarget.Remove(lastContent));
} }
panelTarget.Add(currentContent = content); if (!content.IsAlive)
currentContent.FadeIn(200, Easing.OutQuint); panelTarget.Add(content);
content.FadeIn(200, Easing.OutQuint);
currentContent = content;
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
cancellationToken?.Cancel(); cancellationToken?.Cancel();
@ -203,5 +246,14 @@ namespace osu.Game.Overlays
}); });
} }
} }
protected override void Update()
{
base.Update();
if (shouldAddNextPage)
filterControl.AddPageToResult();
}
} }
} }