mirror of
https://github.com/ppy/osu.git
synced 2026-06-03 18:44:15 +08:00
Merge pull request #34282 from bdach/song-select-favourite
Support (un)favouriting beatmap sets from song select
This commit is contained in:
@@ -5,6 +5,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@@ -50,24 +52,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
((DummyAPIAccess)API).HandleRequest = request =>
|
||||
{
|
||||
switch (request)
|
||||
{
|
||||
case GetBeatmapSetRequest set:
|
||||
if (set.ID == currentOnlineSet?.OnlineID)
|
||||
{
|
||||
set.TriggerSuccess(currentOnlineSet);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new Container
|
||||
@@ -151,6 +135,27 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Test]
|
||||
public void TestOnlineAvailability()
|
||||
{
|
||||
AddStep("set up request handler", () =>
|
||||
{
|
||||
((DummyAPIAccess)API).HandleRequest = request =>
|
||||
{
|
||||
switch (request)
|
||||
{
|
||||
case GetBeatmapSetRequest set:
|
||||
if (set.ID == currentOnlineSet?.OnlineID)
|
||||
{
|
||||
set.TriggerSuccess(currentOnlineSet);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("online beatmapset", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
@@ -159,7 +164,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("play count = 10000", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "10,000");
|
||||
AddAssert("favourites count = 2345", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(1).Text.ToString() == "2,345");
|
||||
AddAssert("favourites count = 2345", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString() == "2,345");
|
||||
AddStep("online beatmapset with local diff", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
@@ -170,7 +175,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("play count = -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "-");
|
||||
AddAssert("favourites count = 2345", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(1).Text.ToString() == "2,345");
|
||||
AddAssert("favourites count = 2345", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString() == "2,345");
|
||||
AddStep("local beatmapset", () =>
|
||||
{
|
||||
var (working, _) = createTestBeatmap();
|
||||
@@ -179,7 +184,75 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddAssert("play count = -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "-");
|
||||
AddAssert("favourites count = -", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(1).Text.ToString() == "-");
|
||||
AddAssert("favourites count = -", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString() == "-");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFavouriting()
|
||||
{
|
||||
var resetEvent = new ManualResetEventSlim(false);
|
||||
|
||||
AddStep("set up request handler", () =>
|
||||
{
|
||||
((DummyAPIAccess)API).HandleRequest = request =>
|
||||
{
|
||||
switch (request)
|
||||
{
|
||||
case GetBeatmapSetRequest set:
|
||||
if (set.ID == currentOnlineSet?.OnlineID)
|
||||
{
|
||||
set.TriggerSuccess(currentOnlineSet);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
case PostBeatmapFavouriteRequest favourite:
|
||||
Task.Run(() =>
|
||||
{
|
||||
resetEvent.Wait(10000);
|
||||
favourite.TriggerSuccess();
|
||||
});
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("online beatmapset", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddUntilStep("play count = 10000", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ElementAt(0).Text.ToString() == "10,000");
|
||||
AddUntilStep("favourites count = 2345", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString() == "2,345");
|
||||
|
||||
AddStep("click favourite button", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().TriggerClick());
|
||||
AddStep("allow request to complete", () => resetEvent.Set());
|
||||
AddAssert("favourites count = 2346", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString() == "2,346");
|
||||
|
||||
AddStep("reset event", () => resetEvent.Reset());
|
||||
AddStep("click favourite button", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().TriggerClick());
|
||||
AddStep("allow request to complete", () => resetEvent.Set());
|
||||
AddAssert("favourites count = 2345", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString() == "2,345");
|
||||
|
||||
AddStep("reset event", () => resetEvent.Reset());
|
||||
AddStep("click favourite button", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().TriggerClick());
|
||||
AddStep("change to another beatmap", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
onlineSet.FavouriteCount = 9999;
|
||||
working.BeatmapSetInfo.OnlineID = onlineSet.OnlineID = 99999;
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddStep("allow request to complete", () => resetEvent.Set());
|
||||
AddAssert("favourites count = 9999", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString() == "9,999");
|
||||
}
|
||||
|
||||
[TestCase(120, 125, null, "120-125 (mostly 120)")]
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
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;
|
||||
@@ -59,7 +58,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
internal string DisplayedArtist => artistLabel.Text.ToString();
|
||||
|
||||
private StatisticPlayCount playCount = null!;
|
||||
private Statistic favouritesStatistic = null!;
|
||||
private FavouriteButton favouriteButton = null!;
|
||||
private Statistic lengthStatistic = null!;
|
||||
private Statistic bpmStatistic = null!;
|
||||
|
||||
@@ -157,7 +156,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Margin = new MarginPadding { Left = -SongSelect.WEDGE_CONTENT_MARGIN },
|
||||
},
|
||||
favouritesStatistic = new Statistic(OsuIcon.Heart, background: true, minSize: 25f)
|
||||
favouriteButton = new FavouriteButton
|
||||
{
|
||||
TooltipText = BeatmapsStrings.StatusFavourites,
|
||||
},
|
||||
@@ -316,20 +315,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting)
|
||||
{
|
||||
playCount.Value = null;
|
||||
favouritesStatistic.Text = null;
|
||||
}
|
||||
else if (currentOnlineBeatmapSet == null)
|
||||
{
|
||||
playCount.Value = new StatisticPlayCount.Data(-1, -1);
|
||||
favouritesStatistic.Text = "-";
|
||||
favouriteButton.SetLoading();
|
||||
}
|
||||
else
|
||||
{
|
||||
var onlineBeatmapSet = currentOnlineBeatmapSet;
|
||||
var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == working.Value.BeatmapInfo.OnlineID);
|
||||
|
||||
var onlineBeatmap = currentOnlineBeatmapSet?.Beatmaps.SingleOrDefault(b => b.OnlineID == working.Value.BeatmapInfo.OnlineID);
|
||||
playCount.Value = new StatisticPlayCount.Data(onlineBeatmap?.PlayCount ?? -1, onlineBeatmap?.UserPlayCount ?? -1);
|
||||
favouritesStatistic.Text = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0");
|
||||
favouriteButton.SetBeatmapSet(currentOnlineBeatmapSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
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.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapTitleWedge
|
||||
{
|
||||
public partial class FavouriteButton : OsuClickableContainer
|
||||
{
|
||||
private readonly BindableBool isFavourite = new BindableBool();
|
||||
|
||||
private Box background = null!;
|
||||
private OsuSpriteText valueText = null!;
|
||||
private LoadingSpinner loadingSpinner = null!;
|
||||
private Box hoverLayer = null!;
|
||||
private Box flashLayer = null!;
|
||||
private SpriteIcon icon = null!;
|
||||
|
||||
private APIBeatmapSet? onlineBeatmapSet;
|
||||
private PostBeatmapFavouriteRequest? favouriteRequest;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
internal LocalisableString Text => valueText.Text;
|
||||
|
||||
public FavouriteButton()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
Shear = OsuGame.SHEAR;
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(0.2f),
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Margin = new MarginPadding { Left = 10, Right = 10, Vertical = 5f },
|
||||
Spacing = new Vector2(4f, 0f),
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
icon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Icon = OsuIcon.Heart,
|
||||
Size = new Vector2(OsuFont.Style.Heading2.Size),
|
||||
Colour = colourProvider.Content2,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Height = 20,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
loadingSpinner = new LoadingSpinner
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(12f),
|
||||
State = { Value = Visibility.Visible },
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize, minSize: 25),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
valueText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Style.Heading2,
|
||||
Colour = colourProvider.Content2,
|
||||
Margin = new MarginPadding { Bottom = 2f },
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
hoverLayer = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Colour = Colour4.White.Opacity(0.1f),
|
||||
Blending = BlendingParameters.Additive,
|
||||
},
|
||||
flashLayer = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Colour = Colour4.White,
|
||||
}
|
||||
});
|
||||
Action = toggleFavourite;
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
hoverLayer.FadeIn(500, Easing.OutQuint);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
hoverLayer.FadeOut(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
// Note: `setLoading()` and `setBeatmapSet()` are called externally via their public counterparts by song select when the beatmap changes,
|
||||
// as well as internally in order to display the progress and result of the (un)favourite operation when the button is clicked.
|
||||
// In case of external calls, we want to cancel pending favourite requests, primarily to avoid a situation when a late success callback from an (un)favourite
|
||||
// could show the favourite count from a prior beatmap.
|
||||
|
||||
public void SetLoading()
|
||||
{
|
||||
if (favouriteRequest?.CompletionState == APIRequestCompletionState.Waiting)
|
||||
favouriteRequest.Cancel();
|
||||
setLoading();
|
||||
}
|
||||
|
||||
private void setLoading()
|
||||
{
|
||||
loadingSpinner.State.Value = Visibility.Visible;
|
||||
valueText.FadeOut(120, Easing.OutQuint);
|
||||
|
||||
onlineBeatmapSet = null;
|
||||
updateFavouriteState();
|
||||
}
|
||||
|
||||
public void SetBeatmapSet(APIBeatmapSet? beatmapSet)
|
||||
{
|
||||
if (favouriteRequest?.CompletionState == APIRequestCompletionState.Waiting)
|
||||
favouriteRequest.Cancel();
|
||||
setBeatmapSet(beatmapSet);
|
||||
}
|
||||
|
||||
private void setBeatmapSet(APIBeatmapSet? beatmapSet)
|
||||
{
|
||||
loadingSpinner.State.Value = Visibility.Hidden;
|
||||
valueText.FadeIn(120, Easing.OutQuint);
|
||||
|
||||
onlineBeatmapSet = beatmapSet;
|
||||
updateFavouriteState();
|
||||
}
|
||||
|
||||
private void updateFavouriteState()
|
||||
{
|
||||
Enabled.Value = onlineBeatmapSet != null;
|
||||
|
||||
if (loadingSpinner.State.Value == Visibility.Hidden)
|
||||
valueText.Text = onlineBeatmapSet?.FavouriteCount.ToLocalisableString(@"N0") ?? "-";
|
||||
|
||||
isFavourite.Value = onlineBeatmapSet?.HasFavourited == true;
|
||||
|
||||
background.FadeColour(isFavourite.Value ? colours.Pink4.Darken(1f).Opacity(0.5f) : Color4.Black.Opacity(0.2f), 500, Easing.OutQuint);
|
||||
icon.FadeColour(isFavourite.Value ? colours.Pink1 : colourProvider.Content2, 500, Easing.OutQuint);
|
||||
valueText.FadeColour(isFavourite.Value ? colours.Pink1 : colourProvider.Content2, 500, Easing.OutQuint);
|
||||
|
||||
icon.Icon = isFavourite.Value ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart;
|
||||
}
|
||||
|
||||
private void toggleFavourite()
|
||||
{
|
||||
Debug.Assert(onlineBeatmapSet != null);
|
||||
|
||||
// having this copy locally is important to capture this particular beatmap set instance rather than the field in the request success callback,
|
||||
// because if it was captured via the field / `this`, it could change value due to an external `setLoading()` or `setBeatmapSet()` call.
|
||||
// there's also the part where we want to call `setLoading()` here to show the spinner, but that also sets `onlineBeatmapSet` to null.
|
||||
var beatmapSet = onlineBeatmapSet;
|
||||
|
||||
favouriteRequest = new PostBeatmapFavouriteRequest(beatmapSet.OnlineID, isFavourite.Value ? BeatmapFavouriteAction.UnFavourite : BeatmapFavouriteAction.Favourite);
|
||||
favouriteRequest.Success += () =>
|
||||
{
|
||||
bool hasFavourited = favouriteRequest.Action == BeatmapFavouriteAction.Favourite;
|
||||
beatmapSet.HasFavourited = hasFavourited;
|
||||
beatmapSet.FavouriteCount += hasFavourited ? 1 : -1;
|
||||
setBeatmapSet(beatmapSet);
|
||||
if (hasFavourited)
|
||||
flashLayer.FadeOutFromOne(500, Easing.OutQuint);
|
||||
};
|
||||
api.Queue(favouriteRequest);
|
||||
setLoading();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user