diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
index 814c0519a3..f92dc0313e 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
@@ -26,6 +26,7 @@ using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania;
@@ -52,6 +53,9 @@ namespace osu.Game.Tests.Visual.Ranking
private RulesetStore rulesetStore = null!;
private BeatmapManager beatmapManager = null!;
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@@ -214,14 +218,10 @@ namespace osu.Game.Tests.Visual.Ranking
{
Tags =
[
- new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", },
- new APITag
- {
- Id = 2, Name = "alt",
- Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.",
- },
- new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", },
- new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", },
+ new APITag { Id = 1, Name = "song representation/simple", Description = "Accessible and straightforward map design.", },
+ new APITag { Id = 2, Name = "style/clean", Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.", },
+ new APITag { Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.", },
+ new APITag { Id = 4, Name = "tap/bursts", Description = "Patterns requiring continuous movement and alternating, typically 9 notes or less.", },
]
}), 500);
return true;
@@ -368,12 +368,16 @@ namespace osu.Game.Tests.Visual.Ranking
private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
{
- Child = new StatisticsPanel
+ Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
- State = { Value = Visibility.Visible },
- Score = { Value = score },
- AchievedScore = score,
+ Child = new StatisticsPanel
+ {
+ RelativeSizeAxes = Axes.Both,
+ State = { Value = Visibility.Visible },
+ Score = { Value = score },
+ AchievedScore = score,
+ },
};
});
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs
index 9174b2a3db..c546c9727c 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs
@@ -2,6 +2,7 @@
// 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.Graphics.Cursor;
using osu.Framework.Testing;
@@ -9,6 +10,7 @@ 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.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.Ranking;
@@ -17,6 +19,9 @@ namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneUserTagControl : OsuTestScene
{
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[SetUpSteps]
@@ -34,11 +39,19 @@ namespace osu.Game.Tests.Visual.Ranking
{
Tags =
[
- new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", },
- new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", },
- new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", },
- new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", },
- new APITag { Id = 5, Name = "mono-heavy", Description = "Features monos used in large amounts.", RulesetId = 1, },
+ new APITag { Id = 0, Name = "uncategorised tag", Description = "This probably isn't real but could be and should be handled.", },
+ new APITag { Id = 1, Name = "song representation/simple", Description = "Accessible and straightforward map design.", },
+ new APITag
+ {
+ Id = 2, Name = "style/clean",
+ Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.",
+ },
+ new APITag
+ {
+ Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.",
+ },
+ new APITag { Id = 4, Name = "tap/bursts", Description = "Patterns requiring continuous movement and alternating, typically 9 notes or less.", },
+ new APITag { Id = 5, Name = "style/mono-heavy", Description = "Features monos used in large amounts.", RulesetId = 1, },
]
}), 500);
return true;
@@ -89,7 +102,7 @@ namespace osu.Game.Tests.Visual.Ranking
RelativeSizeAxes = Axes.Both,
Child = new UserTagControl(Beatmap.Value.BeatmapInfo)
{
- Width = 500,
+ Width = 700,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs
index 0eec04541c..48d225de41 100644
--- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Graphics.UserInterface
private Color4 hoverColour = Color4.White.Opacity(0.1f);
+ protected float ScaleOnMouseDown { get; init; } = 0.75f;
+
///
/// The background colour of the while it is hovered.
///
@@ -119,7 +121,7 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnMouseDown(MouseDownEvent e)
{
- Content.ScaleTo(0.75f, 2000, Easing.OutQuint);
+ Content.ScaleTo(ScaleOnMouseDown, 2000, Easing.OutQuint);
return base.OnMouseDown(e);
}
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 6da731588f..8d5e6c05c3 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -86,6 +86,9 @@ namespace osu.Game.Screens.Ranking
private Sample? popInSample;
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+
protected ResultsScreen(ScoreInfo? score)
{
Score = score;
diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs
index 6e18ae1fe4..8caf8d66b5 100644
--- a/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs
+++ b/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs
@@ -1,15 +1,12 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osuTK;
namespace osu.Game.Screens.Ranking.Statistics
{
@@ -53,7 +50,9 @@ namespace osu.Game.Screens.Ranking.Statistics
Padding = new MarginPadding(5),
Children = new[]
{
- createHeader(item),
+ LocalisableString.IsNullOrEmpty(item.Name)
+ ? Empty()
+ : new StatisticItemHeader { Text = item.Name },
new Container
{
RelativeSizeAxes = Axes.X,
@@ -66,37 +65,5 @@ namespace osu.Game.Screens.Ranking.Statistics
}
};
}
-
- private static Drawable createHeader(StatisticItem item)
- {
- if (LocalisableString.IsNullOrEmpty(item.Name))
- return Empty();
-
- return new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- Height = 20,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(5, 0),
- Children = new Drawable[]
- {
- new Circle
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Height = 9,
- Width = 4,
- Colour = Color4Extensions.FromHex("#00FFAA")
- },
- new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Text = item.Name,
- Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold),
- }
- }
- };
- }
}
}
diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItemHeader.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItemHeader.cs
new file mode 100644
index 0000000000..6b496e10dd
--- /dev/null
+++ b/osu.Game/Screens/Ranking/Statistics/StatisticItemHeader.cs
@@ -0,0 +1,68 @@
+// 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.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+
+namespace osu.Game.Screens.Ranking.Statistics
+{
+ public partial class StatisticItemHeader : CompositeDrawable, IHasText
+ {
+ public LocalisableString Text
+ {
+ get => text;
+ set
+ {
+ if (text == value) return;
+
+ text = value;
+ if (IsLoaded)
+ spriteText.Text = value;
+ }
+ }
+
+ private LocalisableString text;
+ private OsuSpriteText spriteText = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ InternalChild = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 20,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(5, 0),
+ Children = new Drawable[]
+ {
+ new Circle
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Height = 9,
+ Width = 4,
+ Colour = Color4Extensions.FromHex("#00FFAA")
+ },
+ spriteText = new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Text = text,
+ Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold),
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs
index ad868e58f0..c33514e343 100644
--- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs
+++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs
@@ -304,7 +304,10 @@ namespace osu.Game.Screens.Ranking.Statistics
this.FadeOut(250, Easing.OutQuint);
if (wasOpened)
+ {
popOutSample?.Play();
+ this.HidePopover(); // targeted at the user tag control
+ }
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/Ranking/UserTag.cs b/osu.Game/Screens/Ranking/UserTag.cs
index d44e531330..9a93df91b5 100644
--- a/osu.Game/Screens/Ranking/UserTag.cs
+++ b/osu.Game/Screens/Ranking/UserTag.cs
@@ -9,17 +9,24 @@ namespace osu.Game.Screens.Ranking
public record UserTag
{
public long Id { get; }
- public string Name { get; }
+ public string FullName { get; }
+ public string? GroupName { get; }
+ public string DisplayName { get; }
public string Description { get; }
public BindableInt VoteCount { get; } = new BindableInt();
public BindableBool Voted { get; } = new BindableBool();
+ public BindableBool Updating { get; } = new BindableBool();
public UserTag(APITag tag)
{
Id = tag.Id;
- Name = tag.Name;
+ FullName = tag.Name;
Description = tag.Description;
+
+ string[] splitName = FullName.Split('/');
+ GroupName = splitName.Length > 1 ? splitName[0] : null;
+ DisplayName = splitName[^1];
}
}
}
diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs
index 80d487112b..789e2cce9f 100644
--- a/osu.Game/Screens/Ranking/UserTagControl.cs
+++ b/osu.Game/Screens/Ranking/UserTagControl.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
+using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
@@ -19,6 +20,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
+using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Extensions;
@@ -32,8 +34,8 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
+using osu.Game.Screens.Ranking.Statistics;
using osuTK;
-using osuTK.Input;
namespace osu.Game.Screens.Ranking
{
@@ -46,14 +48,16 @@ namespace osu.Game.Screens.Ranking
private readonly Cached layout = new Cached();
private FillFlowContainer tagFlow = null!;
- private LoadingLayer loadingLayer = null!;
private BindableList displayedTags { get; } = new BindableList();
- private BindableList extraTags { get; } = new BindableList();
- private Bindable allTags = null!;
+ private Bindable apiTags = null!;
+ private BindableDictionary relevantTagsById { get; } = new BindableDictionary();
+
private readonly Bindable apiBeatmap = new Bindable();
+ private AddNewTagUserTag addNewTagUserTag = null!;
+
[Resolved]
private IAPIProvider api { get; set; } = null!;
@@ -66,47 +70,57 @@ namespace osu.Game.Screens.Ranking
private void load(SessionStatics sessionStatics)
{
AutoSizeAxes = Axes.Y;
+
InternalChildren = new Drawable[]
{
- new FillFlowContainer
+ new GridContainer
{
- Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Spacing = new Vector2(8),
- Children = new Drawable[]
+ Padding = new MarginPadding(10),
+ ColumnDimensions =
+ [
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize)
+ ],
+ RowDimensions = [new Dimension(GridSizeMode.AutoSize, minSize: 40)],
+ Content = new[]
{
- tagFlow = new FillFlowContainer
+ new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Full,
- LayoutDuration = 300,
- LayoutEasing = Easing.OutQuint,
- Spacing = new Vector2(4),
- },
- new AddTagsButton
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- OnTagSelected = onExtraTagSelected,
- AvailableTags = { BindTarget = extraTags },
- },
- },
- },
- loadingLayer = new LoadingLayer
- {
- RelativeSizeAxes = Axes.Both,
- State = { Value = Visibility.Visible }
+ new FillFlowContainer
+ {
+ Direction = FillDirection.Vertical,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(8),
+ Children = new Drawable[]
+ {
+ tagFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Full,
+ Spacing = new Vector2(4),
+ Child = addNewTagUserTag = new AddNewTagUserTag
+ {
+ AvailableTags = { BindTarget = relevantTagsById },
+ OnTagSelected = toggleVote,
+ },
+ },
+ },
+ },
+ }
+ }
},
};
- allTags = sessionStatics.GetBindable(Static.AllBeatmapTags);
+ apiTags = sessionStatics.GetBindable(Static.AllBeatmapTags);
- if (allTags.Value == null)
+ if (apiTags.Value == null)
{
var listTagsRequest = new ListTagsRequest();
- listTagsRequest.Success += tags => allTags.Value = tags.Tags.ToArray();
+ listTagsRequest.Success += tags => apiTags.Value = tags.Tags.ToArray();
api.Queue(listTagsRequest);
}
@@ -115,28 +129,11 @@ namespace osu.Game.Screens.Ranking
api.Queue(getBeatmapSetRequest);
}
- private void onExtraTagSelected(UserTag tag)
- {
- loadingLayer.Show();
- extraTags.Remove(tag);
-
- var req = new AddBeatmapTagRequest(beatmapInfo.OnlineID, tag.Id);
- req.Success += () =>
- {
- tag.Voted.Value = true;
- tag.VoteCount.Value += 1;
- displayedTags.Add(tag);
- loadingLayer.Hide();
- };
- req.Failure += _ => extraTags.Add(tag);
- api.Queue(req);
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
- allTags.BindValueChanged(_ => updateTags());
+ apiTags.BindValueChanged(_ => updateTags());
apiBeatmap.BindValueChanged(_ => updateTags());
updateTags();
@@ -145,29 +142,32 @@ namespace osu.Game.Screens.Ranking
private void updateTags()
{
- if (allTags.Value == null || apiBeatmap.Value?.TopTags == null)
+ if (apiTags.Value == null || apiBeatmap.Value == null)
return;
- var relevantTagsById = allTags.Value
- .Where(tag => tag.RulesetId == null || tag.RulesetId == beatmapInfo.Ruleset.OnlineID)
- .ToDictionary(t => t.Id);
- var ownTagIds = apiBeatmap.Value.OwnTagIds?.ToHashSet() ?? new HashSet();
+ relevantTagsById.Clear();
+ relevantTagsById.AddRange(apiTags.Value
+ .Where(t => t.RulesetId == null || t.RulesetId == beatmapInfo.Ruleset.OnlineID)
+ .Select(t => new KeyValuePair(t.Id, new UserTag(t))));
- foreach (var topTag in apiBeatmap.Value.TopTags)
+ foreach (var topTag in apiBeatmap.Value.TopTags ?? [])
{
- if (relevantTagsById.Remove(topTag.TagId, out var tag))
+ if (relevantTagsById.TryGetValue(topTag.TagId, out var tag))
{
- displayedTags.Add(new UserTag(tag)
- {
- VoteCount = { Value = topTag.VoteCount },
- Voted = { Value = ownTagIds.Contains(tag.Id) }
- });
+ tag.VoteCount.Value = topTag.VoteCount;
+ tag.Updating.Value = false;
+ displayedTags.Add(tag);
}
}
- extraTags.AddRange(relevantTagsById.Select(t => new UserTag(t.Value)));
-
- loadingLayer.Hide();
+ foreach (long ownTagId in apiBeatmap.Value.OwnTagIds ?? [])
+ {
+ if (relevantTagsById.TryGetValue(ownTagId, out var tag))
+ {
+ tag.Voted.Value = true;
+ tag.Updating.Value = false;
+ }
+ }
}
private void displayTags(object? sender, NotifyCollectionChangedEventArgs e)
@@ -181,7 +181,7 @@ namespace osu.Game.Screens.Ranking
for (int i = 0; i < e.NewItems!.Count; i++)
{
var tag = (UserTag)e.NewItems[i]!;
- var drawableTag = new DrawableUserTag(tag);
+ var drawableTag = new DrawableUserTag(tag) { OnSelected = toggleVote };
tagFlow.Insert(tagFlow.Count, drawableTag);
tag.VoteCount.BindValueChanged(voteCountChanged, true);
layout.Invalidate();
@@ -196,7 +196,7 @@ namespace osu.Game.Screens.Ranking
{
var tag = (UserTag)e.OldItems[i]!;
tag.VoteCount.ValueChanged -= voteCountChanged;
- tagFlow.Remove(oldItems[e.OldStartingIndex + i], true);
+ tagFlow.Remove(oldItems[1 + e.OldStartingIndex + i], true);
}
break;
@@ -205,20 +205,58 @@ namespace osu.Game.Screens.Ranking
case NotifyCollectionChangedAction.Reset:
{
tagFlow.Clear();
+ tagFlow.Add(addNewTagUserTag);
break;
}
}
}
+ private void toggleVote(UserTag tag)
+ {
+ if (tag.Updating.Value)
+ return;
+
+ tag.Updating.Value = true;
+
+ APIRequest request;
+
+ switch (tag.Voted.Value)
+ {
+ case true:
+ var removeReq = new RemoveBeatmapTagRequest(beatmapInfo.OnlineID, tag.Id);
+ removeReq.Success += () =>
+ {
+ tag.VoteCount.Value -= 1;
+ tag.Voted.Value = false;
+ };
+ request = removeReq;
+ break;
+
+ case false:
+ var addReq = new AddBeatmapTagRequest(beatmapInfo.OnlineID, tag.Id);
+ addReq.Success += () =>
+ {
+ tag.VoteCount.Value += 1;
+ tag.Voted.Value = true;
+ if (!displayedTags.Contains(tag))
+ displayedTags.Add(tag);
+ };
+ request = addReq;
+ break;
+ }
+
+ request.Success += () => tag.Updating.Value = false;
+ request.Failure += _ => tag.Updating.Value = false;
+
+ api.Queue(request);
+ }
+
private void voteCountChanged(ValueChangedEvent _)
{
var tagsWithNoVotes = displayedTags.Where(t => t.VoteCount.Value == 0).ToArray();
foreach (var tag in tagsWithNoVotes)
- {
displayedTags.Remove(tag);
- extraTags.Add(tag);
- }
layout.Invalidate();
}
@@ -235,62 +273,71 @@ namespace osu.Game.Screens.Ranking
.Select((tag, index) => new KeyValuePair(tag, index)));
foreach (var drawableTag in tagFlow)
- tagFlow.SetLayoutPosition(drawableTag, sortedTags[drawableTag.UserTag]);
+ {
+ if (drawableTag == addNewTagUserTag)
+ tagFlow.SetLayoutPosition(drawableTag, float.MinValue);
+ else
+ tagFlow.SetLayoutPosition(drawableTag, sortedTags[drawableTag.UserTag]);
+ }
layout.Validate();
}
}
+ protected override bool OnClick(ClickEvent e) => true;
+
private partial class DrawableUserTag : OsuAnimatedButton
{
public readonly UserTag UserTag;
+ public Action? OnSelected { get; set; }
+
private readonly Bindable voteCount = new Bindable();
private readonly BindableBool voted = new BindableBool();
private readonly Bindable confirmed = new BindableBool();
+ private readonly BindableBool updating = new BindableBool();
- private Box mainBackground = null!;
+ protected Box MainBackground { get; private set; } = null!;
private Box voteBackground = null!;
- private OsuSpriteText tagNameText = null!;
- private OsuSpriteText voteCountText = null!;
- private LoadingSpinner spinner = null!;
+
+ protected OsuSpriteText TagCategoryText { get; private set; } = null!;
+ protected OsuSpriteText TagNameText { get; private set; } = null!;
+ protected OsuSpriteText VoteCountText { get; private set; } = null!;
+
+ private readonly bool showVoteCount;
+
+ private LoadingLayer loadingLayer = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
- [Resolved]
- private Bindable beatmap { get; set; } = null!;
-
- [Resolved]
- private IAPIProvider api { get; set; } = null!;
-
- private APIRequest? requestInFlight;
-
- public DrawableUserTag(UserTag userTag)
+ public DrawableUserTag(UserTag userTag, bool showVoteCount = true)
{
UserTag = userTag;
+ this.showVoteCount = showVoteCount;
voteCount.BindTo(userTag.VoteCount);
+ updating.BindTo(userTag.Updating);
voted.BindTo(userTag.Voted);
AutoSizeAxes = Axes.Both;
+
+ ScaleOnMouseDown = 0.95f;
}
[BackgroundDependencyLoader]
private void load()
{
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
- CornerRadius = 8;
+ CornerRadius = 5;
Masking = true;
EdgeEffect = new EdgeEffectParameters
{
Colour = colours.Lime1,
- Radius = 5,
+ Radius = 6,
Type = EdgeEffectType.Glow,
};
Content.AddRange(new Drawable[]
{
- mainBackground = new Box
+ MainBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
@@ -299,42 +346,61 @@ namespace osu.Game.Screens.Ranking
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
- Padding = new MarginPadding { Left = 6, Right = 3, Vertical = 3, },
- Spacing = new Vector2(5),
- Children = new Drawable[]
+ Children = new[]
{
- tagNameText = new OsuSpriteText
+ TagCategoryText = new OsuSpriteText
{
- Text = UserTag.Name,
+ Alpha = UserTag.GroupName != null ? 0.6f : 0,
+ Text = UserTag.GroupName ?? default(LocalisableString),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
+ Margin = new MarginPadding { Horizontal = 6 }
},
new Container
{
AutoSizeAxes = Axes.Both,
- CornerRadius = 5,
- Masking = true,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Children = new Drawable[]
{
- voteBackground = new Box
+ new Box
{
RelativeSizeAxes = Axes.Both,
+ Alpha = 0.1f,
+ Blending = BlendingParameters.Additive,
},
- voteCountText = new OsuSpriteText
+ TagNameText = new OsuSpriteText
{
+ Text = UserTag.DisplayName,
+ Font = OsuFont.Default.With(weight: FontWeight.SemiBold),
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Horizontal = 6, Vertical = 3, },
},
- spinner = new LoadingSpinner(withBox: true)
+ }
+ },
+ showVoteCount
+ ? new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Children = new Drawable[]
{
- Alpha = 0,
- Size = new Vector2(18),
+ voteBackground = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ VoteCountText = new OsuSpriteText
+ {
+ Margin = new MarginPadding { Horizontal = 6, Vertical = 3, },
+ },
}
}
- }
+ : Empty(),
}
- }
+ },
+ loadingLayer = new LoadingLayer(dimBackground: true),
});
TooltipText = UserTag.Description;
@@ -346,100 +412,66 @@ namespace osu.Game.Screens.Ranking
const double transition_duration = 300;
- voteCount.BindValueChanged(_ =>
+ updating.BindValueChanged(u => loadingLayer.State.Value = u.NewValue ? Visibility.Visible : Visibility.Hidden);
+
+ if (showVoteCount)
{
- voteCountText.Text = voteCount.Value.ToLocalisableString();
- confirmed.Value = voteCount.Value >= 10;
- }, true);
- voted.BindValueChanged(v =>
- {
- if (v.NewValue)
+ voteCount.BindValueChanged(_ =>
{
- voteBackground.FadeColour(colours.Lime3, transition_duration, Easing.OutQuint);
- voteCountText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint);
- }
- else
+ VoteCountText.Text = voteCount.Value.ToLocalisableString();
+ confirmed.Value = voteCount.Value >= 10;
+ }, true);
+ voted.BindValueChanged(v =>
{
- voteBackground.FadeColour(colours.Gray2, transition_duration, Easing.OutQuint);
- voteCountText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
- }
- }, true);
- confirmed.BindValueChanged(c =>
- {
- if (c.NewValue)
+ if (v.NewValue)
+ {
+ voteBackground.FadeColour(colours.Lime2, transition_duration, Easing.OutQuint);
+ VoteCountText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint);
+ }
+ else
+ {
+ voteBackground.FadeColour(colours.Gray2, transition_duration, Easing.OutQuint);
+ VoteCountText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
+ }
+ }, true);
+
+ confirmed.BindValueChanged(c =>
{
- mainBackground.FadeColour(colours.Lime1, transition_duration, Easing.OutQuint);
- tagNameText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint);
- FadeEdgeEffectTo(0.5f, transition_duration, Easing.OutQuint);
- }
- else
- {
- mainBackground.FadeColour(colours.Gray4, transition_duration, Easing.OutQuint);
- tagNameText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
- FadeEdgeEffectTo(0f, transition_duration, Easing.OutQuint);
- }
- }, true);
+ if (c.NewValue)
+ {
+ MainBackground.FadeColour(colours.Lime2, transition_duration, Easing.OutQuint);
+ TagCategoryText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint);
+ TagNameText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint);
+ FadeEdgeEffectTo(0.3f, transition_duration, Easing.OutQuint);
+ }
+ else
+ {
+ MainBackground.FadeColour(colours.Gray6, transition_duration, Easing.OutQuint);
+ TagCategoryText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
+ TagNameText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
+ FadeEdgeEffectTo(0f, transition_duration, Easing.OutQuint);
+ }
+ }, true);
+ }
+
FinishTransforms(true);
- Action = () =>
- {
- if (requestInFlight != null)
- return;
-
- spinner.Show();
-
- APIRequest request;
-
- switch (voted.Value)
- {
- case true:
- var removeReq = new RemoveBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, UserTag.Id);
- removeReq.Success += () =>
- {
- voteCount.Value -= 1;
- voted.Value = false;
- };
- request = removeReq;
- break;
-
- case false:
- var addReq = new AddBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, UserTag.Id);
- addReq.Success += () =>
- {
- voteCount.Value += 1;
- voted.Value = true;
- };
- request = addReq;
- break;
- }
-
- request.Success += () =>
- {
- spinner.Hide();
- requestInFlight = null;
- };
- request.Failure += _ =>
- {
- spinner.Hide();
- requestInFlight = null;
- };
- api.Queue(requestInFlight = request);
- };
+ Action = () => OnSelected?.Invoke(UserTag);
}
}
- private partial class AddTagsButton : GrayButton, IHasPopover
+ private partial class AddNewTagUserTag : DrawableUserTag, IHasPopover
{
- public BindableList AvailableTags { get; } = new BindableList();
+ public BindableDictionary AvailableTags { get; } = new BindableDictionary();
public Action? OnTagSelected { get; set; }
- public AddTagsButton()
- : base(FontAwesome.Solid.Plus)
- {
- Size = new Vector2(30);
+ [Resolved]
+ private OverlayColourProvider overlayColourProvider { get; set; } = null!;
- Action = this.ShowPopover;
+ public AddNewTagUserTag()
+ : base(new UserTag(new APITag { Name = "+/add" }), false)
+ {
}
protected override void LoadComplete()
@@ -447,6 +479,12 @@ namespace osu.Game.Screens.Ranking
base.LoadComplete();
AvailableTags.BindCollectionChanged((_, _) => Enabled.Value = AvailableTags.Count > 0, true);
+ Action = this.ShowPopover;
+
+ MainBackground.FadeColour(overlayColourProvider.Background2);
+ TagCategoryText.FadeColour(overlayColourProvider.Colour0);
+ TagNameText.FadeColour(overlayColourProvider.Colour0);
+ FadeEdgeEffectTo(0);
}
public Popover GetPopover() => new AddTagsPopover
@@ -461,37 +499,50 @@ namespace osu.Game.Screens.Ranking
private SearchTextBox searchBox = null!;
private SearchContainer searchContainer = null!;
- public BindableList AvailableTags { get; } = new BindableList();
+ public BindableDictionary AvailableTags { get; } = new BindableDictionary();
public Action? OnSelected { get; set; }
+ private CancellationTokenSource? loadCancellationTokenSource;
+
[BackgroundDependencyLoader]
private void load()
{
- Child = new OsuScrollContainer
+ AllowableAnchors = new[]
{
- Width = 250,
- Height = 250,
- ScrollbarOverlapsContent = false,
- Children = new Drawable[]
+ Anchor.TopCentre,
+ Anchor.BottomCentre,
+ };
+
+ Children = new Drawable[]
+ {
+ new Container
{
- searchBox = new SearchTextBox
+ Size = new Vector2(400, 300),
+ Children = new Drawable[]
{
- HoldFocus = true,
- RelativeSizeAxes = Axes.X,
- },
- searchContainer = new SearchContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Padding = new MarginPadding { Right = 5, Top = 50, },
- Spacing = new Vector2(10),
- ChildrenEnumerable = AvailableTags.Select(tag => new DrawableAddableTag(tag)
+ searchBox = new SearchTextBox
{
- Action = () => select(tag)
- })
- }
+ HoldFocus = true,
+ RelativeSizeAxes = Axes.X,
+ Depth = float.MinValue,
+ },
+ new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ Y = 40,
+ Height = 260,
+ ScrollbarOverlapsContent = false,
+ Child = searchContainer = new SearchContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Right = 5, Bottom = 10 },
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(10),
+ }
+ }
+ },
},
};
}
@@ -500,21 +551,38 @@ namespace osu.Game.Screens.Ranking
{
base.LoadComplete();
+ AvailableTags.BindCollectionChanged((_, _) =>
+ {
+ loadCancellationTokenSource?.Cancel();
+ loadCancellationTokenSource = new CancellationTokenSource();
+
+ LoadComponentsAsync(createItems(AvailableTags.Values), loaded =>
+ {
+ searchContainer.Clear();
+ searchContainer.AddRange(loaded);
+ }, loadCancellationTokenSource.Token);
+ }, true);
searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true);
}
+ private IEnumerable createItems(IEnumerable tags)
+ {
+ var grouped = tags.GroupBy(tag => tag.GroupName).OrderBy(group => group.Key);
+
+ foreach (var group in grouped)
+ {
+ var drawableGroup = new GroupFlow(group.Key);
+
+ foreach (var tag in group.OrderBy(t => t.FullName))
+ drawableGroup.Add(new DrawableAddableTag(tag) { Action = () => OnSelected?.Invoke(tag) });
+
+ yield return drawableGroup;
+ }
+ }
+
public override bool OnPressed(KeyBindingPressEvent e)
{
- if (base.OnPressed(e))
- return true;
-
- if (e.Repeat)
- return false;
-
- if (State.Value == Visibility.Hidden)
- return false;
-
- if (e.Action == GlobalAction.Select)
+ if (e.Action == GlobalAction.Select && !e.Repeat)
{
attemptSelect();
return true;
@@ -523,69 +591,113 @@ namespace osu.Game.Screens.Ranking
return false;
}
- protected override bool OnKeyDown(KeyDownEvent e)
- {
- if (e.Key == Key.Enter)
- {
- attemptSelect();
- return true;
- }
-
- return base.OnKeyDown(e);
- }
-
private void attemptSelect()
{
- var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray();
+ var visibleItems = searchContainer.ChildrenOfType().Where(d => d.IsPresent).ToArray();
if (visibleItems.Length == 1)
- select(visibleItems.Single().Tag);
+ OnSelected?.Invoke(visibleItems.Single().Tag);
}
- private void select(UserTag tag)
+ private partial class GroupFlow : FillFlowContainer, IFilterable
{
- OnSelected?.Invoke(tag);
- this.HidePopover();
+ public IEnumerable FilterTerms { get; }
+
+ public bool MatchingFilter
+ {
+ set => Alpha = value ? 1 : 0;
+ }
+
+ public bool FilteringActive { set { } }
+
+ public GroupFlow(string? name)
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ Direction = FillDirection.Vertical;
+ Spacing = new Vector2(5);
+
+ Add(new StatisticItemHeader { Text = name ?? "uncategorised" });
+
+ FilterTerms = name == null ? [] : [name];
+ }
}
private partial class DrawableAddableTag : OsuAnimatedButton, IFilterable
{
public readonly UserTag Tag;
+ private Box votedBackground = null!;
+ private SpriteIcon votedIcon = null!;
+
+ private readonly Bindable voted = new Bindable();
+ private readonly BindableBool updating = new BindableBool();
+
+ private LoadingLayer loadingLayer = null!;
+
public DrawableAddableTag(UserTag tag)
{
Tag = tag;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- Anchor = Origin = Anchor.Centre;
+
+ ScaleOnMouseDown = 0.95f;
+
+ voted.BindTo(Tag.Voted);
+ updating.BindTo(Tag.Updating);
}
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
[BackgroundDependencyLoader]
- private void load(OsuColour colours, OverlayColourProvider? colourProvider)
+ private void load()
{
Content.AddRange(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = colourProvider?.Background3 ?? colours.GreySeaFoamDark,
+ Colour = colours.Gray7,
Depth = float.MaxValue,
},
+ new Container
+ {
+ RelativeSizeAxes = Axes.Y,
+ Width = 30,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Depth = float.MaxValue,
+ Children = new Drawable[]
+ {
+ votedBackground = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ votedIcon = new SpriteIcon
+ {
+ Size = new Vector2(16),
+ Icon = FontAwesome.Solid.ThumbsUp,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ }
+ },
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
- Padding = new MarginPadding(5),
+ Padding = new MarginPadding(5) { Right = 35 },
Children = new Drawable[]
{
- new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold))
+ new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Text = Tag.Name,
+ Text = Tag.DisplayName,
},
new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14))
{
@@ -594,14 +706,29 @@ namespace osu.Game.Screens.Ranking
Text = Tag.Description,
}
}
- }
+ },
+ loadingLayer = new LoadingLayer(dimBackground: true),
});
}
- public IEnumerable FilterTerms => [Tag.Name, Tag.Description];
+ public IEnumerable FilterTerms => [Tag.FullName, Tag.Description];
public bool MatchingFilter { set => Alpha = value ? 1 : 0; }
public bool FilteringActive { set { } }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ voted.BindValueChanged(_ =>
+ {
+ votedBackground.FadeColour(voted.Value ? colours.Lime2 : colours.Gray2, 250, Easing.OutQuint);
+ votedIcon.FadeColour(voted.Value ? Colour4.Black : Colour4.White, 250, Easing.OutQuint);
+ }, true);
+ FinishTransforms(true);
+
+ updating.BindValueChanged(u => loadingLayer.State.Value = u.NewValue ? Visibility.Visible : Visibility.Hidden);
+ }
}
}
}