diff --git a/osu.Android.props b/osu.Android.props index b9b127309a..b9ef783b2a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -53,7 +53,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas-overlay.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas-overlay.png new file mode 100644 index 0000000000..3a6612378e Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas-overlay.png differ diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas.png new file mode 100644 index 0000000000..afb8698b2d Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/fruit-bananas.png differ diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs new file mode 100644 index 0000000000..7c05d99c59 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Overlays; +using NUnit.Framework; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneBeatmapListingOverlay : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(BeatmapListingOverlay), + }; + + protected override bool UseOnlineAPI => true; + + private readonly BeatmapListingOverlay overlay; + + public TestSceneBeatmapListingOverlay() + { + Add(overlay = new BeatmapListingOverlay()); + } + + [Test] + public void TestShow() + { + AddStep("Show", overlay.Show); + } + + [Test] + public void TestHide() + { + AddStep("Hide", overlay.Hide); + } + } +} diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 5652b8d2bd..930ca8fdf1 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.ComponentModel; +using osu.Framework.IO.Network; using osu.Game.Overlays; using osu.Game.Overlays.Direct; using osu.Game.Rulesets; @@ -26,8 +27,21 @@ namespace osu.Game.Online.API.Requests this.direction = direction; } - // ReSharper disable once ImpureMethodCallOnReadonlyValueField - protected override string Target => $@"beatmapsets/search?q={query}&m={ruleset.ID ?? 0}&s={searchCategory.ToString().ToLowerInvariant()}&sort={sortCriteria.ToString().ToLowerInvariant()}_{directionString}"; + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.AddParameter("q", query); + + if (ruleset.ID.HasValue) + req.AddParameter("m", ruleset.ID.Value.ToString()); + + req.AddParameter("s", searchCategory.ToString().ToLowerInvariant()); + req.AddParameter("sort", $"{sortCriteria.ToString().ToLowerInvariant()}_{directionString}"); + + return req; + } + + protected override string Target => @"beatmapsets/search"; } public enum BeatmapSearchCategory diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs index 28863cb0e0..3c4fb11ed1 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs @@ -2,12 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using Newtonsoft.Json; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { public class SearchBeatmapSetsResponse : ResponseWithCursor { + [JsonProperty("beatmapsets")] public IEnumerable BeatmapSets; + + [JsonProperty("total")] + public int Total; } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs new file mode 100644 index 0000000000..5af92914de --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs @@ -0,0 +1,24 @@ +// 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.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.BeatmapListing +{ + public class BeatmapListingHeader : OverlayHeader + { + protected override ScreenTitle CreateTitle() => new BeatmapListingTitle(); + + private class BeatmapListingTitle : ScreenTitle + { + public BeatmapListingTitle() + { + Title = @"beatmap"; + Section = @"listing"; + } + + protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog"); + } + } +} diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs index f47144e5d8..f9799d8a6b 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs @@ -104,6 +104,8 @@ namespace osu.Game.Overlays.BeatmapListing } } }); + + Category.Value = BeatmapSearchCategory.Leaderboard; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs index cb41b33bc4..27c43b092a 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs @@ -8,16 +8,17 @@ using osu.Framework.Graphics; using osuTK.Graphics; using osuTK; using osu.Framework.Input.Events; +using osu.Game.Overlays.Direct; namespace osu.Game.Overlays.BeatmapListing { - public class BeatmapListingSortTabControl : OverlaySortTabControl + public class BeatmapListingSortTabControl : OverlaySortTabControl { public readonly Bindable SortDirection = new Bindable(Overlays.SortDirection.Descending); public BeatmapListingSortTabControl() { - Current.Value = BeatmapSortCriteria.Ranked; + Current.Value = DirectSortCriteria.Ranked; } protected override SortTabControl CreateControl() => new BeatmapSortTabControl @@ -29,7 +30,7 @@ namespace osu.Game.Overlays.BeatmapListing { public readonly Bindable SortDirection = new Bindable(); - protected override TabItem CreateTabItem(BeatmapSortCriteria value) => new BeatmapSortTabItem(value) + protected override TabItem CreateTabItem(DirectSortCriteria value) => new BeatmapSortTabItem(value) { SortDirection = { BindTarget = SortDirection } }; @@ -39,12 +40,12 @@ namespace osu.Game.Overlays.BeatmapListing { public readonly Bindable SortDirection = new Bindable(); - public BeatmapSortTabItem(BeatmapSortCriteria value) + public BeatmapSortTabItem(DirectSortCriteria value) : base(value) { } - protected override TabButton CreateTabButton(BeatmapSortCriteria value) => new BeatmapTabButton(value) + protected override TabButton CreateTabButton(DirectSortCriteria value) => new BeatmapTabButton(value) { Active = { BindTarget = Active }, SortDirection = { BindTarget = SortDirection } @@ -66,7 +67,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly SpriteIcon icon; - public BeatmapTabButton(BeatmapSortCriteria value) + public BeatmapTabButton(DirectSortCriteria value) : base(value) { Add(icon = new SpriteIcon @@ -104,15 +105,4 @@ namespace osu.Game.Overlays.BeatmapListing } } } - - public enum BeatmapSortCriteria - { - Title, - Artist, - Difficulty, - Ranked, - Rating, - Plays, - Favourites, - } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs new file mode 100644 index 0000000000..213e9a4244 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -0,0 +1,299 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Threading; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.BeatmapListing; +using osu.Game.Overlays.Direct; +using osu.Game.Rulesets; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays +{ + public class BeatmapListingOverlay : FullscreenOverlay + { + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + private SearchBeatmapSetsRequest getSetsRequest; + + private Container panelsPlaceholder; + private Drawable currentContent; + private BeatmapListingSearchSection searchSection; + private BeatmapListingSortTabControl sortControl; + + public BeatmapListingOverlay() + : base(OverlayColourScheme.Blue) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background6 + }, + new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new ReverseChildIDFillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0.25f), + Type = EdgeEffectType.Shadow, + Radius = 3, + Offset = new Vector2(0f, 1f), + }, + Children = new Drawable[] + { + new BeatmapListingHeader(), + searchSection = new BeatmapListingSearchSection(), + } + }, + new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background4, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + Height = 40, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background5 + }, + sortControl = new BeatmapListingSortTabControl + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 20 } + } + } + }, + panelsPlaceholder = new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding { Horizontal = 20 }, + } + } + } + } + } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + var sortCriteria = sortControl.Current; + var sortDirection = sortControl.SortDirection; + + searchSection.Query.BindValueChanged(query => + { + sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? DirectSortCriteria.Ranked : DirectSortCriteria.Relevance; + sortDirection.Value = SortDirection.Descending; + + queueUpdateSearch(true); + }); + + searchSection.Ruleset.BindValueChanged(_ => queueUpdateSearch()); + searchSection.Category.BindValueChanged(_ => queueUpdateSearch()); + sortCriteria.BindValueChanged(_ => queueUpdateSearch()); + sortDirection.BindValueChanged(_ => queueUpdateSearch()); + } + + private ScheduledDelegate queryChangedDebounce; + + private void queueUpdateSearch(bool queryTextChanged = false) + { + getSetsRequest?.Cancel(); + + queryChangedDebounce?.Cancel(); + queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); + } + + private void updateSearch() + { + if (!IsLoaded) + return; + + if (State.Value == Visibility.Hidden) + return; + + if (API == null) + return; + + previewTrackManager.StopAnyPlaying(this); + + currentContent?.FadeColour(Color4.DimGray, 400, Easing.OutQuint); + + getSetsRequest = new SearchBeatmapSetsRequest( + searchSection.Query.Value, + searchSection.Ruleset.Value, + searchSection.Category.Value, + sortControl.Current.Value, + sortControl.SortDirection.Value); + + getSetsRequest.Success += response => Schedule(() => recreatePanels(response)); + + API.Queue(getSetsRequest); + } + + private void recreatePanels(SearchBeatmapSetsResponse response) + { + if (response.Total == 0) + { + searchSection.BeatmapSet = null; + LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder); + return; + } + + var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList(); + + var newPanels = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + Alpha = 0, + Margin = new MarginPadding { Vertical = 15 }, + ChildrenEnumerable = beatmaps.Select(b => new DirectGridPanel(b) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }) + }; + + LoadComponentAsync(newPanels, loaded => + { + addContentToPlaceholder(loaded); + searchSection.BeatmapSet = beatmaps.First(); + }); + } + + private void addContentToPlaceholder(Drawable content) + { + Drawable lastContent = currentContent; + + if (lastContent != null) + { + lastContent.FadeOut(100, Easing.OutQuint).Expire(); + + // 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. + lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y); + } + + panelsPlaceholder.Add(currentContent = content); + currentContent.FadeIn(200, Easing.OutQuint); + } + + protected override void Dispose(bool isDisposing) + { + getSetsRequest?.Cancel(); + queryChangedDebounce?.Cancel(); + + base.Dispose(isDisposing); + } + + private class NotFoundDrawable : CompositeDrawable + { + public NotFoundDrawable() + { + RelativeSizeAxes = Axes.X; + Height = 250; + Alpha = 0; + 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, + Text = @"... nope, nothing found.", + } + } + }); + } + } + } +} diff --git a/osu.Game/Overlays/Direct/FilterControl.cs b/osu.Game/Overlays/Direct/FilterControl.cs index 8b04bf0387..70a3ab54fb 100644 --- a/osu.Game/Overlays/Direct/FilterControl.cs +++ b/osu.Game/Overlays/Direct/FilterControl.cs @@ -34,14 +34,13 @@ namespace osu.Game.Overlays.Direct public enum DirectSortCriteria { - Relevance, Title, Artist, - Creator, Difficulty, Ranked, Rating, Plays, Favourites, + Relevance, } } diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 8f753fd3aa..b878aba489 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -75,8 +75,6 @@ namespace osu.Game.Overlays.Music }, }; - list.Items.BindTo(beatmapSets); - filter.Search.OnCommit = (sender, newText) => { BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault(); @@ -87,7 +85,13 @@ namespace osu.Game.Overlays.Music beatmap.Value.Track.Restart(); } }; + } + protected override void LoadComplete() + { + base.LoadComplete(); + + list.Items.BindTo(beatmapSets); beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo, true); } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 7c7daf6eb9..d788929739 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -25,7 +25,16 @@ namespace osu.Game.Overlays [Resolved] private BeatmapManager beatmaps { get; set; } - public IBindableList BeatmapSets => beatmapSets; + public IBindableList BeatmapSets + { + get + { + if (LoadState < LoadState.Ready) + throw new InvalidOperationException($"{nameof(BeatmapSets)} should not be accessed before the music controller is loaded."); + + return beatmapSets; + } + } /// /// Point in time after which the current track will be restarted on triggering a "previous track" action. @@ -54,16 +63,18 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { - beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next())); beatmaps.ItemAdded += handleBeatmapAdded; beatmaps.ItemRemoved += handleBeatmapRemoved; + + beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next())); } protected override void LoadComplete() { + base.LoadComplete(); + beatmap.BindValueChanged(beatmapChanged, true); mods.BindValueChanged(_ => ResetTrackAdjustments(), true); - base.LoadComplete(); } /// @@ -82,11 +93,16 @@ namespace osu.Game.Overlays /// public bool IsPlaying => current?.Track.IsRunning ?? false; - private void handleBeatmapAdded(BeatmapSetInfo set) => - Schedule(() => beatmapSets.Add(set)); + private void handleBeatmapAdded(BeatmapSetInfo set) => Schedule(() => + { + if (!beatmapSets.Contains(set)) + beatmapSets.Add(set); + }); - private void handleBeatmapRemoved(BeatmapSetInfo set) => - Schedule(() => beatmapSets.RemoveAll(s => s.ID == set.ID)); + private void handleBeatmapRemoved(BeatmapSetInfo set) => Schedule(() => + { + beatmapSets.RemoveAll(s => s.ID == set.ID); + }); private ScheduledDelegate seekDelegate; diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index dfcf99d30c..118cb037cb 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -58,6 +58,9 @@ namespace osu.Game.Overlays [Resolved] private Bindable beatmap { get; set; } + [Resolved] + private OsuColour colours { get; set; } + public NowPlayingOverlay() { Width = 400; @@ -65,7 +68,7 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { Children = new Drawable[] { @@ -182,15 +185,15 @@ namespace osu.Game.Overlays } } }; - - playlist.BeatmapSets.BindTo(musicController.BeatmapSets); - playlist.State.ValueChanged += s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint); } protected override void LoadComplete() { base.LoadComplete(); + playlist.BeatmapSets.BindTo(musicController.BeatmapSets); + playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true); + beatmap.BindDisabledChanged(beatmapDisabledChanged, true); musicController.TrackChanged += trackChanged; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index bd5219b872..6b8f8fee8c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 2c1aff7d3c..f68bedc57f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -73,7 +73,7 @@ - +