2020-02-19 14:40:54 +00:00
// 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.
2020-03-06 00:09:43 +00:00
using System.Collections.Generic ;
2020-02-19 14:40:54 +00:00
using System.Linq ;
2020-03-06 00:09:43 +00:00
using System.Threading ;
2020-05-14 06:35:11 +00:00
using System.Threading.Tasks ;
2020-02-19 14:40:54 +00:00
using osu.Framework.Allocation ;
2020-05-14 06:35:11 +00:00
using osu.Framework.Extensions.IEnumerableExtensions ;
2021-06-22 05:53:21 +00:00
using osu.Framework.Localisation ;
2020-02-19 14:40:54 +00:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using osu.Framework.Graphics.Shapes ;
2020-02-19 15:17:02 +00:00
using osu.Framework.Graphics.Sprites ;
using osu.Framework.Graphics.Textures ;
2020-04-21 06:47:43 +00:00
using osu.Framework.Input.Events ;
2020-02-19 14:40:54 +00:00
using osu.Game.Audio ;
2021-10-23 14:55:05 +00:00
using osu.Game.Beatmaps.Drawables.Cards ;
2021-06-19 12:54:24 +00:00
using osu.Game.Graphics ;
2020-02-19 14:40:54 +00:00
using osu.Game.Graphics.Sprites ;
2021-06-19 12:54:24 +00:00
using osu.Game.Graphics.Containers ;
2021-12-22 12:16:36 +00:00
using osu.Game.Online.API.Requests.Responses ;
2020-02-19 14:40:54 +00:00
using osu.Game.Overlays.BeatmapListing ;
2021-06-16 04:46:13 +00:00
using osu.Game.Resources.Localisation.Web ;
2020-02-19 14:40:54 +00:00
using osuTK ;
using osuTK.Graphics ;
namespace osu.Game.Overlays
{
2021-01-18 08:13:38 +00:00
public class BeatmapListingOverlay : OnlineOverlay < BeatmapListingHeader >
2020-02-19 14:40:54 +00:00
{
[Resolved]
private PreviewTrackManager previewTrackManager { get ; set ; }
2020-02-20 02:08:42 +00:00
private Drawable currentContent ;
2020-03-06 00:09:43 +00:00
private Container panelTarget ;
2021-11-27 16:53:57 +00:00
private FillFlowContainer < BeatmapCard > foundContent ;
2020-05-11 18:18:47 +00:00
private NotFoundDrawable notFoundContent ;
2021-06-19 12:54:24 +00:00
private SupporterRequiredDrawable supporterRequiredContent ;
2021-01-18 07:48:12 +00:00
private BeatmapListingFilterControl filterControl ;
2020-02-19 14:40:54 +00:00
public BeatmapListingOverlay ( )
: base ( OverlayColourScheme . Blue )
{
}
[BackgroundDependencyLoader]
private void load ( )
{
2021-01-18 07:48:12 +00:00
Child = new FillFlowContainer
2020-02-19 14:40:54 +00:00
{
2021-01-18 07:48:12 +00:00
RelativeSizeAxes = Axes . X ,
AutoSizeAxes = Axes . Y ,
Direction = FillDirection . Vertical ,
Children = new Drawable [ ]
2020-02-19 14:40:54 +00:00
{
2021-01-18 07:48:12 +00:00
filterControl = new BeatmapListingFilterControl
{
TypingStarted = onTypingStarted ,
SearchStarted = onSearchStarted ,
SearchFinished = onSearchFinished ,
} ,
new Container
2020-02-19 14:40:54 +00:00
{
AutoSizeAxes = Axes . Y ,
RelativeSizeAxes = Axes . X ,
Children = new Drawable [ ]
{
2021-01-18 07:48:12 +00:00
new Box
2020-02-19 14:40:54 +00:00
{
2021-01-18 07:48:12 +00:00
RelativeSizeAxes = Axes . Both ,
2021-10-23 14:55:05 +00:00
Colour = ColourProvider . Background5 ,
2020-02-19 14:40:54 +00:00
} ,
2021-01-18 07:48:12 +00:00
panelTarget = new Container
2020-02-19 14:40:54 +00:00
{
AutoSizeAxes = Axes . Y ,
RelativeSizeAxes = Axes . X ,
2021-09-11 13:54:49 +00:00
Masking = true ,
2021-01-18 07:48:12 +00:00
Padding = new MarginPadding { Horizontal = 20 } ,
2020-02-19 14:40:54 +00:00
Children = new Drawable [ ]
{
2021-11-27 16:53:57 +00:00
foundContent = new FillFlowContainer < BeatmapCard > ( ) ,
2021-01-18 07:48:12 +00:00
notFoundContent = new NotFoundDrawable ( ) ,
2021-06-19 12:54:24 +00:00
supporterRequiredContent = new SupporterRequiredDrawable ( ) ,
2020-02-19 14:40:54 +00:00
}
}
2021-01-18 07:48:12 +00:00
} ,
2021-01-04 13:42:39 +00:00
} ,
2020-02-19 14:40:54 +00:00
}
} ;
}
2021-12-22 12:16:36 +00:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
filterControl . CardSize . BindValueChanged ( _ = > onCardSizeChanged ( ) ) ;
}
2021-07-01 10:45:17 +00:00
public void ShowWithSearch ( string query )
{
filterControl . Search ( query ) ;
Show ( ) ;
}
2020-02-19 14:40:54 +00:00
2021-01-18 07:48:12 +00:00
protected override BeatmapListingHeader CreateHeader ( ) = > new BeatmapListingHeader ( ) ;
2020-02-19 14:40:54 +00:00
2021-02-09 09:32:44 +00:00
protected override Color4 BackgroundColour = > ColourProvider . Background6 ;
2020-02-19 14:40:54 +00:00
2020-11-10 07:26:30 +00:00
private void onTypingStarted ( )
{
// temporary until the textbox/header is updated to always stay on screen.
2021-01-18 07:48:12 +00:00
ScrollFlow . ScrollToStart ( ) ;
2020-02-19 23:54:35 +00:00
}
2020-04-21 06:47:43 +00:00
protected override void OnFocus ( FocusEvent e )
2020-02-19 23:54:35 +00:00
{
2020-04-21 06:47:43 +00:00
base . OnFocus ( e ) ;
2020-02-19 23:54:35 +00:00
2020-04-21 06:47:43 +00:00
filterControl . TakeFocus ( ) ;
2020-02-19 14:40:54 +00:00
}
2020-03-06 00:09:43 +00:00
private CancellationTokenSource cancellationToken ;
2020-02-19 14:40:54 +00:00
2020-03-06 00:09:43 +00:00
private void onSearchStarted ( )
2020-02-19 23:54:35 +00:00
{
2020-03-06 00:09:43 +00:00
cancellationToken ? . Cancel ( ) ;
2020-02-19 14:40:54 +00:00
previewTrackManager . StopAnyPlaying ( this ) ;
2020-03-06 00:09:43 +00:00
if ( panelTarget . Any ( ) )
2021-01-18 07:48:12 +00:00
Loading . Show ( ) ;
2020-02-19 14:40:54 +00:00
}
2020-05-14 06:35:11 +00:00
private Task panelLoadDelegate ;
2021-06-22 05:53:21 +00:00
private void onSearchFinished ( BeatmapListingFilterControl . SearchResult searchResult )
2020-02-19 14:40:54 +00:00
{
2021-06-26 18:31:26 +00:00
if ( searchResult . Type = = BeatmapListingFilterControl . SearchResultType . SupporterOnlyFilters )
2020-02-19 14:40:54 +00:00
{
2021-06-26 18:31:26 +00:00
supporterRequiredContent . UpdateText ( searchResult . SupporterOnlyFiltersUsed ) ;
2021-06-22 05:53:21 +00:00
addContentToPlaceholder ( supporterRequiredContent ) ;
2020-02-19 14:40:54 +00:00
return ;
}
2021-12-22 12:16:36 +00:00
var newCards = createCardsFor ( searchResult . Results ) ;
2020-02-19 15:17:02 +00:00
2020-05-14 06:35:11 +00:00
if ( filterControl . CurrentPage = = 0 )
2020-02-19 14:40:54 +00:00
{
2020-05-14 06:35:11 +00:00
//No matches case
2021-12-22 12:16:36 +00:00
if ( ! newCards . Any ( ) )
2020-02-19 14:40:54 +00:00
{
2021-06-22 05:53:21 +00:00
addContentToPlaceholder ( notFoundContent ) ;
2020-05-14 06:35:11 +00:00
return ;
}
2020-02-19 14:40:54 +00:00
2021-12-22 12:16:36 +00:00
var content = createCardContainerFor ( newCards ) ;
2020-05-11 18:18:47 +00:00
2020-05-14 06:35:11 +00:00
panelLoadDelegate = LoadComponentAsync ( foundContent = content , addContentToPlaceholder , ( cancellationToken = new CancellationTokenSource ( ) ) . Token ) ;
2020-05-11 18:18:47 +00:00
}
else
2020-02-19 14:40:54 +00:00
{
2021-12-22 12:16:36 +00:00
panelLoadDelegate = LoadComponentsAsync ( newCards , loaded = >
2020-05-11 18:18:47 +00:00
{
2020-05-14 06:35:11 +00:00
lastFetchDisplayedTime = Time . Current ;
foundContent . AddRange ( loaded ) ;
loaded . ForEach ( p = > p . FadeIn ( 200 , Easing . OutQuint ) ) ;
2020-05-11 18:18:47 +00:00
} ) ;
}
2020-02-19 15:17:02 +00:00
}
2021-12-22 12:16:36 +00:00
private BeatmapCard [ ] createCardsFor ( IEnumerable < APIBeatmapSet > beatmapSets ) = > beatmapSets . Select ( set = > BeatmapCard . Create ( set , filterControl . CardSize . Value ) . With ( c = >
{
c . Anchor = Anchor . TopCentre ;
c . Origin = Anchor . TopCentre ;
} ) ) . ToArray ( ) ;
private static ReverseChildIDFillFlowContainer < BeatmapCard > createCardContainerFor ( IEnumerable < BeatmapCard > newCards )
{
// spawn new children with the contained so we only clear old content at the last moment.
// reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most).
var content = new ReverseChildIDFillFlowContainer < BeatmapCard >
{
RelativeSizeAxes = Axes . X ,
AutoSizeAxes = Axes . Y ,
Spacing = new Vector2 ( 10 ) ,
Alpha = 0 ,
Margin = new MarginPadding { Vertical = 15 } ,
ChildrenEnumerable = newCards
} ;
return content ;
}
2020-02-19 15:17:02 +00:00
private void addContentToPlaceholder ( Drawable content )
{
2021-01-18 07:48:12 +00:00
Loading . Hide ( ) ;
2020-05-14 06:35:11 +00:00
lastFetchDisplayedTime = Time . Current ;
2020-02-21 07:13:24 +00:00
2021-01-25 19:11:50 +00:00
if ( content = = currentContent )
return ;
2020-02-21 07:13:24 +00:00
2020-03-06 00:09:43 +00:00
var lastContent = currentContent ;
2020-02-20 04:48:34 +00:00
if ( lastContent ! = null )
{
2021-09-11 13:54:49 +00:00
lastContent . FadeOut ( 100 , Easing . OutQuint ) ;
2020-02-20 04:48:34 +00:00
2021-09-11 13:54:49 +00:00
// Consider the case when the new content is smaller than the last content.
// 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.
// 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.
var sequence = lastContent . Delay ( 25 ) . Schedule ( ( ) = > lastContent . BypassAutoSizeAxes = Axes . Y ) ;
2021-12-22 12:16:36 +00:00
if ( lastContent = = foundContent )
{
sequence . Then ( ) . Schedule ( ( ) = >
{
foundContent . Expire ( ) ;
foundContent = null ;
} ) ;
}
2020-02-20 04:48:34 +00:00
}
2020-05-11 18:18:47 +00:00
if ( ! content . IsAlive )
panelTarget . Add ( content ) ;
2021-01-26 06:42:48 +00:00
content . FadeInFromZero ( 200 , Easing . OutQuint ) ;
2020-05-11 18:18:47 +00:00
currentContent = content ;
2021-09-11 13:54:49 +00:00
// currentContent may be one of the placeholders, and still have BypassAutoSizeAxes set to Y from the last fade-out.
// restore to the initial state.
currentContent . BypassAutoSizeAxes = Axes . None ;
2020-02-19 15:17:02 +00:00
}
2021-12-22 12:16:36 +00:00
private void onCardSizeChanged ( )
{
if ( foundContent = = null | | ! foundContent . Any ( ) )
return ;
Loading . Show ( ) ;
var newCards = createCardsFor ( foundContent . Reverse ( ) . Select ( card = > card . BeatmapSet ) ) ;
panelLoadDelegate = LoadComponentsAsync ( newCards , cards = >
{
foundContent . Clear ( ) ;
foundContent . AddRange ( cards ) ;
Loading . Hide ( ) ;
} , ( cancellationToken = new CancellationTokenSource ( ) ) . Token ) ;
}
2020-02-19 15:17:02 +00:00
protected override void Dispose ( bool isDisposing )
{
2020-03-06 00:09:43 +00:00
cancellationToken ? . Cancel ( ) ;
2020-02-19 15:17:02 +00:00
base . Dispose ( isDisposing ) ;
}
2021-01-25 20:41:05 +00:00
public class NotFoundDrawable : CompositeDrawable
2020-02-19 15:17:02 +00:00
{
Fix placeholder drawables on beatmap listing not always hiding correctly
`BeatmapListingOverlay.addContentToPlaceholder()`, in order to make
transitions between different beatmap listing content (whether it is
actual cards, or placeholders for no beatmaps found/supporter-specific
filters chosen), would set `BypassAutoSizeAxes = Y` on content as it is
fading out, to make the transition smoother. The property in question
was supposed to be getting restored to `None` on the next show.
In testing scenarios, it sometimes turned out that this wasn't the case,
therefore making the placeholders effectively not show - while they
were present and fully opaque, they would be the only child of
an auto-sized container with `BypassAutoSizeAxes = Y`, so the parent
auto-sized to a zero height, which logically follows from the premise,
but is not what was desired.
This in turn was caused by the fact that the `BypassAutoSizeAxes = Y`
set was scheduled, and sometimes it would be scheduled in such a way
that the drawable would cease to be present on the next frame due to its
alpha being past the cutoff point of 0.0001. Therefore the scheduled set
would not execute until the *next* time the placeholder was shown,
therefore causing the bug.
Fix by ensuring that the placeholder drawables are always present if
their schedulers have any tasks enqueued, on top of the usual checks of
alpha and scale performed via the base implementation.
2021-12-22 14:05:23 +00:00
// required for scheduled tasks to complete correctly
// (see `addContentToPlaceholder()` and the scheduled `BypassAutoSizeAxes` set during fade-out in outer class above)
public override bool IsPresent = > base . IsPresent | | Scheduler . HasPendingTasks ;
2020-02-19 23:43:13 +00:00
public NotFoundDrawable ( )
2020-02-19 15:17:02 +00:00
{
RelativeSizeAxes = Axes . X ;
Height = 250 ;
2020-02-20 02:08:42 +00:00
Alpha = 0 ;
2020-02-19 15:17:02 +00:00
Margin = new MarginPadding { Top = 15 } ;
}
[BackgroundDependencyLoader]
private void load ( TextureStore textures )
{
AddInternal ( new FillFlowContainer
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
RelativeSizeAxes = Axes . Y ,
AutoSizeAxes = Axes . X ,
Direction = FillDirection . Horizontal ,
Spacing = new Vector2 ( 10 , 0 ) ,
Children = new Drawable [ ]
{
new Sprite
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
RelativeSizeAxes = Axes . Both ,
FillMode = FillMode . Fit ,
Texture = textures . Get ( @"Online/not-found" )
} ,
new OsuSpriteText
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
2021-06-16 04:46:13 +00:00
Text = BeatmapsStrings . ListingSearchNotFoundQuote ,
2020-02-19 15:17:02 +00:00
}
}
} ) ;
}
2020-02-19 14:40:54 +00:00
}
2020-05-11 18:18:47 +00:00
2021-06-26 18:40:54 +00:00
// TODO: localisation requires Text/LinkFlowContainer support for localising strings with links inside
// (https://github.com/ppy/osu-framework/issues/4530)
2021-06-19 12:54:24 +00:00
public class SupporterRequiredDrawable : CompositeDrawable
{
Fix placeholder drawables on beatmap listing not always hiding correctly
`BeatmapListingOverlay.addContentToPlaceholder()`, in order to make
transitions between different beatmap listing content (whether it is
actual cards, or placeholders for no beatmaps found/supporter-specific
filters chosen), would set `BypassAutoSizeAxes = Y` on content as it is
fading out, to make the transition smoother. The property in question
was supposed to be getting restored to `None` on the next show.
In testing scenarios, it sometimes turned out that this wasn't the case,
therefore making the placeholders effectively not show - while they
were present and fully opaque, they would be the only child of
an auto-sized container with `BypassAutoSizeAxes = Y`, so the parent
auto-sized to a zero height, which logically follows from the premise,
but is not what was desired.
This in turn was caused by the fact that the `BypassAutoSizeAxes = Y`
set was scheduled, and sometimes it would be scheduled in such a way
that the drawable would cease to be present on the next frame due to its
alpha being past the cutoff point of 0.0001. Therefore the scheduled set
would not execute until the *next* time the placeholder was shown,
therefore causing the bug.
Fix by ensuring that the placeholder drawables are always present if
their schedulers have any tasks enqueued, on top of the usual checks of
alpha and scale performed via the base implementation.
2021-12-22 14:05:23 +00:00
// required for scheduled tasks to complete correctly
// (see `addContentToPlaceholder()` and the scheduled `BypassAutoSizeAxes` set during fade-out in outer class above)
public override bool IsPresent = > base . IsPresent | | Scheduler . HasPendingTasks ;
2021-06-24 05:45:38 +00:00
private LinkFlowContainer supporterRequiredText ;
2021-06-20 13:23:54 +00:00
2021-06-19 12:54:24 +00:00
public SupporterRequiredDrawable ( )
{
RelativeSizeAxes = Axes . X ;
2021-06-20 09:17:07 +00:00
Height = 225 ;
2021-06-19 12:54:24 +00:00
Alpha = 0 ;
}
[BackgroundDependencyLoader]
private void load ( TextureStore textures )
{
AddInternal ( new FillFlowContainer
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
RelativeSizeAxes = Axes . Y ,
AutoSizeAxes = Axes . X ,
Direction = FillDirection . Horizontal ,
2021-06-24 05:45:38 +00:00
Children = new Drawable [ ]
2021-06-19 12:54:24 +00:00
{
new Sprite
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
RelativeSizeAxes = Axes . Both ,
FillMode = FillMode . Fit ,
Texture = textures . Get ( @"Online/supporter-required" ) ,
} ,
2021-06-24 05:45:38 +00:00
supporterRequiredText = new LinkFlowContainer
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
AutoSizeAxes = Axes . Both ,
Margin = new MarginPadding { Bottom = 10 } ,
} ,
2021-06-19 12:54:24 +00:00
}
} ) ;
}
2021-06-22 05:53:21 +00:00
public void UpdateText ( List < LocalisableString > filters )
2021-06-21 07:31:47 +00:00
{
2021-06-24 05:45:38 +00:00
supporterRequiredText . Clear ( ) ;
2021-06-19 12:54:24 +00:00
2021-06-24 05:45:38 +00:00
supporterRequiredText . AddText (
BeatmapsStrings . ListingSearchSupporterFilterQuoteDefault ( string . Join ( " and " , filters ) , "" ) . ToString ( ) ,
2021-06-22 05:53:21 +00:00
t = >
{
t . Font = OsuFont . GetFont ( size : 16 ) ;
t . Colour = Colour4 . White ;
}
2021-06-24 05:45:38 +00:00
) ;
2021-06-22 05:53:21 +00:00
supporterRequiredText . AddLink ( BeatmapsStrings . ListingSearchSupporterFilterQuoteLinkText . ToString ( ) , @"/store/products/supporter-tag" ) ;
2021-06-19 12:54:24 +00:00
}
}
2020-05-14 06:35:11 +00:00
private const double time_between_fetches = 500 ;
private double lastFetchDisplayedTime ;
2020-05-11 18:18:47 +00:00
protected override void Update ( )
{
base . Update ( ) ;
2020-05-14 06:35:11 +00:00
const int pagination_scroll_distance = 500 ;
bool shouldShowMore = panelLoadDelegate ? . IsCompleted ! = false
& & Time . Current - lastFetchDisplayedTime > time_between_fetches
2021-01-18 07:48:12 +00:00
& & ( ScrollFlow . ScrollableExtent > 0 & & ScrollFlow . IsScrolledToEnd ( pagination_scroll_distance ) ) ;
2020-05-14 06:35:11 +00:00
if ( shouldShowMore )
filterControl . FetchNextPage ( ) ;
2020-05-11 18:18:47 +00:00
}
2020-02-19 14:40:54 +00:00
}
}