1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-13 18:47:27 +08:00

Merge pull request #28991 from Layendan/master

Add collection and favourite buttons to results screen
This commit is contained in:
Dean Herbert 2024-08-21 23:59:26 +09:00 committed by GitHub
commit 2876cb73bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 522 additions and 56 deletions

View File

@ -0,0 +1,67 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Ranking;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneCollectionButton : OsuManualInputManagerTestScene
{
private CollectionButton? collectionButton;
private readonly BeatmapInfo beatmapInfo = new BeatmapInfo { OnlineID = 88 };
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create button", () => Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = collectionButton = new CollectionButton(beatmapInfo)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
});
}
[Test]
public void TestCollectionButton()
{
AddStep("click collection button", () =>
{
InputManager.MoveMouseTo(collectionButton!);
InputManager.Click(MouseButton.Left);
});
AddAssert("collection popover is visible", () => this.ChildrenOfType<CollectionPopover>().Single().State.Value == Visibility.Visible);
AddStep("click outside popover", () =>
{
InputManager.MoveMouseTo(ScreenSpaceDrawQuad.TopLeft);
InputManager.Click(MouseButton.Left);
});
AddAssert("collection popover is hidden", () => this.ChildrenOfType<CollectionPopover>().Single().State.Value == Visibility.Hidden);
AddStep("click collection button", () =>
{
InputManager.MoveMouseTo(collectionButton!);
InputManager.Click(MouseButton.Left);
});
AddStep("press escape", () => InputManager.Key(Key.Escape));
AddAssert("collection popover is hidden", () => this.ChildrenOfType<CollectionPopover>().Single().State.Value == Visibility.Hidden);
}
}
}

View File

@ -0,0 +1,82 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.Ranking;
namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneFavouriteButton : OsuTestScene
{
private FavouriteButton? favourite;
private readonly BeatmapSetInfo beatmapSetInfo = new BeatmapSetInfo { OnlineID = 88 };
private readonly BeatmapSetInfo invalidBeatmapSetInfo = new BeatmapSetInfo();
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create button", () => Child = favourite = new FavouriteButton(beatmapSetInfo)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddStep("register request handling", () => dummyAPI.HandleRequest = request =>
{
if (!(request is GetBeatmapSetRequest beatmapSetRequest)) return false;
beatmapSetRequest.TriggerSuccess(new APIBeatmapSet
{
OnlineID = beatmapSetRequest.ID,
HasFavourited = false,
FavouriteCount = 0,
});
return true;
});
}
[Test]
public void TestLoggedOutIn()
{
AddStep("log out", () => API.Logout());
checkEnabled(false);
AddStep("log in", () =>
{
API.Login("test", "test");
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
});
checkEnabled(true);
}
[Test]
public void TestInvalidBeatmap()
{
AddStep("make beatmap invalid", () => Child = favourite = new FavouriteButton(invalidBeatmapSetInfo)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddStep("log in", () =>
{
API.Login("test", "test");
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
});
checkEnabled(false);
}
private void checkEnabled(bool expected)
{
AddAssert("is " + (expected ? "enabled" : "disabled"), () => favourite!.Enabled.Value == expected);
}
}
}

View File

@ -0,0 +1,81 @@
// 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;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK;
using Realms;
namespace osu.Game.Screens.Ranking
{
public partial class CollectionButton : GrayButton, IHasPopover
{
private readonly BeatmapInfo beatmapInfo;
private readonly Bindable<bool> isInAnyCollection;
[Resolved]
private RealmAccess realmAccess { get; set; } = null!;
private IDisposable? collectionSubscription;
[Resolved]
private OsuColour colours { get; set; } = null!;
public CollectionButton(BeatmapInfo beatmapInfo)
: base(FontAwesome.Solid.Book)
{
this.beatmapInfo = beatmapInfo;
isInAnyCollection = new Bindable<bool>(false);
Size = new Vector2(75, 30);
TooltipText = "collections";
}
[BackgroundDependencyLoader]
private void load()
{
Action = this.ShowPopover;
}
protected override void LoadComplete()
{
base.LoadComplete();
collectionSubscription = realmAccess.RegisterForNotifications(r => r.All<BeatmapCollection>(), collectionsChanged);
isInAnyCollection.BindValueChanged(_ => updateState(), true);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
collectionSubscription?.Dispose();
}
private void collectionsChanged(IRealmCollection<BeatmapCollection> sender, ChangeSet? changes)
{
isInAnyCollection.Value = sender.AsEnumerable().Any(c => c.BeatmapMD5Hashes.Contains(beatmapInfo.MD5Hash));
}
private void updateState()
{
Background.FadeColour(isInAnyCollection.Value ? colours.Green : colours.Gray4, 500, Easing.InOutExpo);
}
public Popover GetPopover() => new CollectionPopover(beatmapInfo);
}
}

View File

@ -0,0 +1,67 @@
// 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Ranking
{
public partial class CollectionPopover : OsuPopover
{
private readonly BeatmapInfo beatmapInfo;
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
public CollectionPopover(BeatmapInfo beatmapInfo)
: base(false)
{
this.beatmapInfo = beatmapInfo;
Body.CornerRadius = 4;
}
[BackgroundDependencyLoader]
private void load()
{
Children = new[]
{
new OsuMenu(Direction.Vertical, true)
{
Items = items,
},
};
}
protected override void OnFocusLost(FocusLostEvent e)
{
base.OnFocusLost(e);
Hide();
}
private OsuMenuItem[] items
{
get
{
var collectionItems = realm.Realm.All<BeatmapCollection>()
.OrderBy(c => c.Name)
.AsEnumerable()
.Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast<OsuMenuItem>().ToList();
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, () => manageCollectionsDialog?.Show()));
return collectionItems.ToArray();
}
}
}
}

View File

@ -0,0 +1,155 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics;
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.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Screens.Ranking
{
public partial class FavouriteButton : GrayButton
{
public readonly BeatmapSetInfo BeatmapSetInfo;
private APIBeatmapSet? beatmapSet;
private readonly Bindable<BeatmapSetFavouriteState> current;
private PostBeatmapFavouriteRequest? favouriteRequest;
private LoadingLayer loading = null!;
private readonly IBindable<APIUser> localUser = new Bindable<APIUser>();
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
public FavouriteButton(BeatmapSetInfo beatmapSetInfo)
: base(FontAwesome.Regular.Heart)
{
BeatmapSetInfo = beatmapSetInfo;
current = new BindableWithCurrent<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(false, 0));
Size = new Vector2(75, 30);
}
[BackgroundDependencyLoader]
private void load()
{
Add(loading = new LoadingLayer(true, false));
Action = toggleFavouriteStatus;
}
protected override void LoadComplete()
{
base.LoadComplete();
current.BindValueChanged(_ => updateState(), true);
localUser.BindTo(api.LocalUser);
localUser.BindValueChanged(_ => updateUser(), true);
}
private void getBeatmapSet()
{
GetBeatmapSetRequest beatmapSetRequest = new GetBeatmapSetRequest(BeatmapSetInfo.OnlineID);
loading.Show();
beatmapSetRequest.Success += beatmapSet =>
{
this.beatmapSet = beatmapSet;
current.Value = new BeatmapSetFavouriteState(this.beatmapSet.HasFavourited, this.beatmapSet.FavouriteCount);
loading.Hide();
Enabled.Value = true;
};
beatmapSetRequest.Failure += e =>
{
Logger.Error(e, $"Failed to fetch beatmap info: {e.Message}");
Schedule(() =>
{
loading.Hide();
Enabled.Value = false;
});
};
api.Queue(beatmapSetRequest);
}
private void toggleFavouriteStatus()
{
if (beatmapSet == null)
return;
Enabled.Value = false;
loading.Show();
var actionType = current.Value.Favourited ? BeatmapFavouriteAction.UnFavourite : BeatmapFavouriteAction.Favourite;
favouriteRequest?.Cancel();
favouriteRequest = new PostBeatmapFavouriteRequest(beatmapSet.OnlineID, actionType);
favouriteRequest.Success += () =>
{
bool favourited = actionType == BeatmapFavouriteAction.Favourite;
current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1));
Enabled.Value = true;
loading.Hide();
};
favouriteRequest.Failure += e =>
{
Logger.Error(e, $"Failed to {actionType.ToString().ToLowerInvariant()} beatmap: {e.Message}");
Schedule(() =>
{
Enabled.Value = true;
loading.Hide();
});
};
api.Queue(favouriteRequest);
}
private void updateUser()
{
if (!(localUser.Value is GuestUser) && BeatmapSetInfo.OnlineID > 0)
getBeatmapSet();
else
{
Enabled.Value = false;
current.Value = new BeatmapSetFavouriteState(false, 0);
updateState();
TooltipText = BeatmapsetsStrings.ShowDetailsFavouriteLogin;
}
}
private void updateState()
{
if (current.Value.Favourited)
{
Background.Colour = colours.Green;
Icon.Icon = FontAwesome.Solid.Heart;
TooltipText = BeatmapsetsStrings.ShowDetailsUnfavourite;
}
else
{
Background.Colour = colours.Gray4;
Icon.Icon = FontAwesome.Regular.Heart;
TooltipText = BeatmapsetsStrings.ShowDetailsFavourite;
}
}
}
}

View File

@ -12,6 +12,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
@ -76,7 +77,7 @@ namespace osu.Game.Screens.Ranking
/// <summary>
/// Whether the user's personal statistics should be shown on the extended statistics panel
/// after clicking the score panel associated with the <see cref="ResultsScreen.Score"/> being presented.
/// after clicking the score panel associated with the <see cref="Score"/> being presented.
/// Requires <see cref="Score"/> to be present.
/// </summary>
public bool ShowUserStatistics { get; init; }
@ -97,73 +98,77 @@ namespace osu.Game.Screens.Ranking
popInSample = audio.Samples.Get(@"UI/overlay-pop-in");
InternalChild = new GridContainer
InternalChild = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
Child = new GridContainer
{
new Drawable[]
RelativeSizeAxes = Axes.Both,
Content = new[]
{
VerticalScrollContent = new VerticalScrollContainer
new Drawable[]
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new Container
VerticalScrollContent = new VerticalScrollContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
StatisticsPanel = createStatisticsPanel().With(panel =>
{
panel.RelativeSizeAxes = Axes.Both;
panel.Score.BindTarget = SelectedScore;
}),
ScorePanelList = new ScorePanelList
{
RelativeSizeAxes = Axes.Both,
SelectedScore = { BindTarget = SelectedScore },
PostExpandAction = () => StatisticsPanel.ToggleVisibility()
},
detachedPanelContainer = new Container<ScorePanel>
{
RelativeSizeAxes = Axes.Both
},
}
}
},
},
new[]
{
bottomPanel = new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = TwoLayerButton.SIZE_EXTENDED.Y,
Alpha = 0,
Children = new Drawable[]
{
new Box
ScrollbarVisible = false,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#333")
},
buttons = new FillFlowContainer
Children = new Drawable[]
{
StatisticsPanel = createStatisticsPanel().With(panel =>
{
panel.RelativeSizeAxes = Axes.Both;
panel.Score.BindTarget = SelectedScore;
}),
ScorePanelList = new ScorePanelList
{
RelativeSizeAxes = Axes.Both,
SelectedScore = { BindTarget = SelectedScore },
PostExpandAction = () => StatisticsPanel.ToggleVisibility()
},
detachedPanelContainer = new Container<ScorePanel>
{
RelativeSizeAxes = Axes.Both
},
}
}
},
},
new[]
{
bottomPanel = new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = TwoLayerButton.SIZE_EXTENDED.Y,
Alpha = 0,
Children = new Drawable[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
Direction = FillDirection.Horizontal
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#333")
},
buttons = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
Direction = FillDirection.Horizontal
},
}
}
}
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
}
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
}
};
@ -202,6 +207,12 @@ namespace osu.Game.Screens.Ranking
},
});
}
if (Score?.BeatmapInfo != null)
buttons.Add(new CollectionButton(Score.BeatmapInfo));
if (Score?.BeatmapInfo?.BeatmapSet != null && Score.BeatmapInfo.BeatmapSet.OnlineID > 0)
buttons.Add(new FavouriteButton(Score.BeatmapInfo.BeatmapSet));
}
protected override void LoadComplete()

View File

@ -9,6 +9,7 @@ using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@ -200,7 +201,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
{
var baseBeatmap = getBeatmapSetRequest.Type == BeatmapSetLookupType.BeatmapId
? beatmapManager.QueryBeatmap(b => b.OnlineID == getBeatmapSetRequest.ID)
: beatmapManager.QueryBeatmap(b => b.BeatmapSet.OnlineID == getBeatmapSetRequest.ID);
: beatmapManager.QueryBeatmapSet(s => s.OnlineID == getBeatmapSetRequest.ID)?.PerformRead(s => s.Beatmaps.First().Detach());
if (baseBeatmap == null)
{

View File

@ -306,7 +306,9 @@ namespace osu.Game.Tests.Visual
StarRating = original.StarRating,
DifficultyName = original.DifficultyName,
}
}
},
HasFavourited = false,
FavouriteCount = 0,
};
foreach (var beatmap in result.Beatmaps)