1
0
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:
Dean Herbert
2025-07-20 18:36:33 +09:00
committed by GitHub
Unverified
3 changed files with 343 additions and 34 deletions
@@ -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)")]
+5 -13
View File
@@ -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();
}
}
}
}