mirror of
https://github.com/ppy/osu.git
synced 2026-05-20 22:21:10 +08:00
Merge branch 'master' into song-select-v2-wedges-leaderboard
This commit is contained in:
@@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and extra lives!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +426,31 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("countdown started", () => MultiplayerClient.ServerRoom!.ActiveCountdowns.Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingsRemainsOpenOnRoomUpdate()
|
||||
{
|
||||
AddStep("set playlist", () =>
|
||||
{
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for room join", () => RoomJoined);
|
||||
|
||||
AddStep("open settings", () => this.ChildrenOfType<MultiplayerMatchSettingsOverlay>().Single().Show());
|
||||
AddAssert("settings opened", () => this.ChildrenOfType<MultiplayerMatchSettingsOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
|
||||
AddStep("trigger room update", () => MultiplayerClient.AddPlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0].Clone()));
|
||||
AddAssert("settings still open", () => this.ChildrenOfType<MultiplayerMatchSettingsOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
}
|
||||
|
||||
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
|
||||
{
|
||||
[Resolved(canBeNull: true)]
|
||||
|
||||
@@ -6,8 +6,12 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
@@ -16,10 +20,12 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
@@ -153,10 +159,40 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFreeModSelectionDisable()
|
||||
{
|
||||
FooterButtonFreeMods freeMods = null!;
|
||||
|
||||
AddAssert("freestyle enabled", () => songSelect.Freestyle.Value, () => Is.True);
|
||||
AddStep("click icon in free mods button", () =>
|
||||
{
|
||||
freeMods = this.ChildrenOfType<FooterButtonFreeMods>().Single();
|
||||
InputManager.MoveMouseTo(freeMods.ChildrenOfType<SpriteIcon>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("mod select not visible", () => this.ChildrenOfType<FreeModSelectOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
|
||||
AddStep("toggle freestyle off", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<FooterButtonFreestyle>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("freestyle disabled", () => songSelect.Freestyle.Value, () => Is.False);
|
||||
AddStep("click icon in free mods button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(freeMods.ChildrenOfType<SpriteIcon>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("mod select visible", () => this.ChildrenOfType<FreeModSelectOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
}
|
||||
|
||||
private partial class TestPlaylistsSongSelect : PlaylistsSongSelect
|
||||
{
|
||||
public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;
|
||||
|
||||
public new IBindable<bool> Freestyle => base.Freestyle;
|
||||
|
||||
public TestPlaylistsSongSelect(Room room)
|
||||
: base(room)
|
||||
{
|
||||
|
||||
@@ -218,7 +218,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
}
|
||||
|
||||
private void waitForLoad()
|
||||
=> AddUntilStep("wait for panels to load", () => this.ChildrenOfType<LoadingSpinner>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
=> AddUntilStep("wait for panels to load", () => this.ChildrenOfType<LoadingSpinner>().First().State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
|
||||
private void assertVisiblePanelCount<T>(int expectedVisible)
|
||||
where T : UserPanel
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapMetadataWedge : SongSelectComponentsTestScene
|
||||
{
|
||||
private APIBeatmapSet? currentOnlineSet;
|
||||
|
||||
private BeatmapMetadataWedge wedge = null!;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
Child = wedge = new BeatmapMetadataWedge
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowHide()
|
||||
{
|
||||
AddStep("all metrics", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
|
||||
AddStep("hide wedge", () => wedge.Hide());
|
||||
AddStep("show wedge", () => wedge.Show());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVariousMetrics()
|
||||
{
|
||||
AddStep("all metrics", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddStep("null beatmap", () => Beatmap.SetDefault());
|
||||
AddStep("no source", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
working.Metadata.Source = string.Empty;
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddStep("no success rate", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
onlineSet.Beatmaps.Single().PlayCount = 0;
|
||||
onlineSet.Beatmaps.Single().PassCount = 0;
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddStep("no user ratings", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
onlineSet.Ratings = Array.Empty<int>();
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddStep("no fail times", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
onlineSet.Beatmaps.Single().FailTimes = null;
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddStep("no metrics", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
onlineSet.Ratings = Array.Empty<int>();
|
||||
onlineSet.Beatmaps.Single().FailTimes = null;
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
AddStep("local beatmap", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
working.BeatmapInfo.OnlineID = 0;
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTruncation()
|
||||
{
|
||||
AddStep("long text", () =>
|
||||
{
|
||||
var (working, onlineSet) = createTestBeatmap();
|
||||
|
||||
working.BeatmapInfo.Metadata.Author = new RealmUser { Username = "Verrrrryyyy llooonngggggg author" };
|
||||
working.BeatmapInfo.Metadata.Source = "Verrrrryyyy llooonngggggg source";
|
||||
working.BeatmapInfo.Metadata.Tags = string.Join(' ', Enumerable.Repeat(working.BeatmapInfo.Metadata.Tags, 3));
|
||||
onlineSet.Genre = new BeatmapSetOnlineGenre { Id = 12, Name = "Verrrrryyyy llooonngggggg genre" };
|
||||
onlineSet.Language = new BeatmapSetOnlineLanguage { Id = 12, Name = "Verrrrryyyy llooonngggggg language" };
|
||||
|
||||
currentOnlineSet = onlineSet;
|
||||
Beatmap.Value = working;
|
||||
});
|
||||
}
|
||||
|
||||
private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap()
|
||||
{
|
||||
var working = CreateWorkingBeatmap(Ruleset.Value);
|
||||
var onlineSet = new APIBeatmapSet
|
||||
{
|
||||
OnlineID = working.BeatmapSetInfo.OnlineID,
|
||||
Genre = new BeatmapSetOnlineGenre { Id = 15, Name = "Pop" },
|
||||
Language = new BeatmapSetOnlineLanguage { Id = 15, Name = "English" },
|
||||
Ratings = Enumerable.Range(0, 11).ToArray(),
|
||||
Beatmaps = new[]
|
||||
{
|
||||
new APIBeatmap
|
||||
{
|
||||
OnlineID = working.BeatmapInfo.OnlineID,
|
||||
PlayCount = 10000,
|
||||
PassCount = 4567,
|
||||
FailTimes = new APIFailTimes
|
||||
{
|
||||
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
|
||||
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now;
|
||||
working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now;
|
||||
return (working, onlineSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Visual.SongSelect;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapTitleWedge : SongSelectComponentsTestScene
|
||||
{
|
||||
private RulesetStore rulesets = null!;
|
||||
|
||||
private BeatmapTitleWedge titleWedge = null!;
|
||||
private BeatmapTitleWedge.DifficultyDisplay difficultyDisplay => titleWedge.ChildrenOfType<BeatmapTitleWedge.DifficultyDisplay>().Single();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
this.rulesets = rulesets;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
titleWedge = new BeatmapTitleWedge
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
AddSliderStep("change star difficulty", 0, 11.9, 4.18, v =>
|
||||
{
|
||||
difficultyDisplay.ChildrenOfType<StarRatingDisplay>().Single().Current.Value = new StarDifficulty(v, 0);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRulesetChange()
|
||||
{
|
||||
selectBeatmap(Beatmap.Value.Beatmap);
|
||||
|
||||
AddWaitStep("wait for select", 3);
|
||||
|
||||
foreach (var rulesetInfo in rulesets.AvailableRulesets)
|
||||
{
|
||||
var testBeatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(rulesetInfo);
|
||||
|
||||
setRuleset(rulesetInfo);
|
||||
selectBeatmap(testBeatmap);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNullBeatmap()
|
||||
{
|
||||
selectBeatmap(null);
|
||||
AddAssert("check default title", () => titleWedge.DisplayedTitle == Beatmap.Default.BeatmapInfo.Metadata.Title);
|
||||
AddAssert("check default artist", () => titleWedge.DisplayedArtist == Beatmap.Default.BeatmapInfo.Metadata.Artist);
|
||||
AddAssert("check empty version", () => string.IsNullOrEmpty(difficultyDisplay.DisplayedVersion.ToString()));
|
||||
AddAssert("check empty author", () => string.IsNullOrEmpty(difficultyDisplay.DisplayedAuthor.ToString()));
|
||||
AddAssert("check no statistics", () => difficultyDisplay.ChildrenOfType<BeatmapTitleWedge.DifficultyStatisticsDisplay>().All(d => !d.Statistics.Any()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBPMUpdates()
|
||||
{
|
||||
const double bpm = 120;
|
||||
IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
|
||||
|
||||
OsuModDoubleTime doubleTime = null!;
|
||||
|
||||
selectBeatmap(beatmap);
|
||||
checkDisplayedBPM($"{bpm}");
|
||||
|
||||
AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() });
|
||||
checkDisplayedBPM($"{bpm * 1.5f}");
|
||||
|
||||
AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2);
|
||||
checkDisplayedBPM($"{bpm * 2}");
|
||||
|
||||
AddStep("select HT", () => SelectedMods.Value = new[] { new OsuModHalfTime() });
|
||||
checkDisplayedBPM($"{bpm * 0.75f}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWedgeVisibility()
|
||||
{
|
||||
AddStep("hide", () => { titleWedge.Hide(); });
|
||||
AddWaitStep("wait for hide", 3);
|
||||
AddAssert("check visibility", () => titleWedge.Alpha == 0);
|
||||
AddStep("show", () => { titleWedge.Show(); });
|
||||
AddWaitStep("wait for show", 1);
|
||||
AddAssert("check visibility", () => titleWedge.Alpha > 0);
|
||||
}
|
||||
|
||||
[TestCase(120, 125, null, "120-125 (mostly 120)")]
|
||||
[TestCase(120, 120.6, null, "120-121 (mostly 120)")]
|
||||
[TestCase(120, 120.4, null, "120")]
|
||||
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180")]
|
||||
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
|
||||
{
|
||||
IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
|
||||
beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm });
|
||||
beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
|
||||
|
||||
if (mod != null)
|
||||
AddStep($"select {mod}", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateModFromAcronym(mod) });
|
||||
|
||||
selectBeatmap(beatmap);
|
||||
checkDisplayedBPM(expectedDisplay);
|
||||
}
|
||||
|
||||
private void setRuleset(RulesetInfo rulesetInfo)
|
||||
{
|
||||
AddStep("set ruleset", () => Ruleset.Value = rulesetInfo);
|
||||
}
|
||||
|
||||
private void selectBeatmap(IBeatmap? b)
|
||||
{
|
||||
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
|
||||
{
|
||||
Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
|
||||
});
|
||||
}
|
||||
|
||||
private void checkDisplayedBPM(string target)
|
||||
{
|
||||
AddUntilStep($"displayed bpm is {target}", () =>
|
||||
{
|
||||
var label = titleWedge.ChildrenOfType<BeatmapTitleWedge.Statistic>().Single(l => l.TooltipText == BeatmapsetsStrings.ShowStatsBpm);
|
||||
return label.Text == target;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneBeatmapTitleWedgeStatistic : ThemeComparisonTestScene
|
||||
{
|
||||
private BeatmapTitleWedge.StatisticPlayCount playCount = null!;
|
||||
private BeatmapTitleWedge.Statistic statistic2 = null!;
|
||||
private BeatmapTitleWedge.Statistic statistic3 = null!;
|
||||
private BeatmapTitleWedge.Statistic statistic4 = null!;
|
||||
|
||||
public TestSceneBeatmapTitleWedgeStatistic()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLoading()
|
||||
{
|
||||
AddStep("setup", () => CreateThemedContent(OverlayColourScheme.Aquamarine));
|
||||
AddStep("set loading", () => this.ChildrenOfType<BeatmapTitleWedge.Statistic>().ForEach(s => s.Text = null));
|
||||
AddWaitStep("wait", 3);
|
||||
AddStep("set values", () =>
|
||||
{
|
||||
playCount.Value = new BeatmapTitleWedge.StatisticPlayCount.Data(1234, 12);
|
||||
statistic2.Text = "3,234";
|
||||
statistic3.Text = "12:34";
|
||||
statistic4.Text = "123";
|
||||
});
|
||||
|
||||
AddStep("set large values", () =>
|
||||
{
|
||||
playCount.Value = new BeatmapTitleWedge.StatisticPlayCount.Data(134587921, 502);
|
||||
statistic2.Text = "1,048,576";
|
||||
statistic3.Text = "2:50:23";
|
||||
statistic4.Text = "1238014";
|
||||
});
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
playCount = new BeatmapTitleWedge.StatisticPlayCount(true, minSize: 50)
|
||||
{
|
||||
Value = new BeatmapTitleWedge.StatisticPlayCount.Data(1234, 12),
|
||||
},
|
||||
statistic2 = new BeatmapTitleWedge.Statistic(OsuIcon.Clock, true, minSize: 30)
|
||||
{
|
||||
Text = "3,234",
|
||||
TooltipText = "Statistic 2",
|
||||
},
|
||||
statistic3 = new BeatmapTitleWedge.Statistic(OsuIcon.Metronome)
|
||||
{
|
||||
Text = "12:34",
|
||||
Margin = new MarginPadding { Right = 10f },
|
||||
TooltipText = "Statistic 3",
|
||||
},
|
||||
statistic4 = new BeatmapTitleWedge.Statistic(OsuIcon.Graphics)
|
||||
{
|
||||
Text = "123",
|
||||
TooltipText = "Statistic 4",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
public partial class TestSceneDifficultyStatisticsDisplay : OsuTestScene
|
||||
{
|
||||
private Container displayContainer = null!;
|
||||
private BeatmapTitleWedge.DifficultyStatisticsDisplay display = null!;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("setup", () =>
|
||||
{
|
||||
Child = displayContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 300,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
},
|
||||
display = new BeatmapTitleWedge.DifficultyStatisticsDisplay
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Statistics = new[]
|
||||
{
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.5f, 0.5f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.8f, 0.8f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.7f, 0.7f, 1f),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
AddSliderStep("display width", 0, 300, 300, v =>
|
||||
{
|
||||
if (displayContainer.IsNotNull())
|
||||
displayContainer.Width = v;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmpty()
|
||||
{
|
||||
AddStep("set empty", () => display.Statistics = Array.Empty<BeatmapTitleWedge.StatisticDifficulty.Data>());
|
||||
AddAssert("no statistics", () => !display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().Any());
|
||||
AddAssert("no tiny statistics", () => !display.ChildrenOfType<GridContainer>().Single().Content.Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisplay()
|
||||
{
|
||||
AddStep("change data with same labels", () => display.Statistics = new[]
|
||||
{
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.7f, 0.7f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.4f, 0.8f, 1f),
|
||||
});
|
||||
|
||||
AddStep("change data with different labels", () => display.Statistics = new[]
|
||||
{
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 4", 0.3f, 0.3f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 5", 0.8f, 0.8f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 6", 0.5f, 0.5f, 1f),
|
||||
});
|
||||
|
||||
AddAssert("statistics visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
|
||||
AddAssert("tiny statistics hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
|
||||
|
||||
AddStep("shrink width", () => displayContainer.Width = 100);
|
||||
AddAssert("statistics hidden", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 0);
|
||||
AddUntilStep("tiny statistics displayed", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContraction()
|
||||
{
|
||||
AddAssert("statistics visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
|
||||
AddAssert("tiny statistics hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
|
||||
|
||||
AddStep("set too many statistics", () => display.Statistics = new[]
|
||||
{
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.7f, 0.7f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.4f, 0.8f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 4", 0.3f, 0.3f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 5", 0.8f, 0.8f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 6", 0.5f, 0.5f, 1f),
|
||||
});
|
||||
|
||||
AddAssert("statistics hidden", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 0);
|
||||
AddUntilStep("tiny statistics displayed", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 1);
|
||||
|
||||
AddStep("set less statistics", () => display.Statistics = new[]
|
||||
{
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
|
||||
});
|
||||
|
||||
AddAssert("tiny statistics hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
|
||||
AddUntilStep("statistics visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAutoSize()
|
||||
{
|
||||
AddStep("setup auto size", () => Child = display = new BeatmapTitleWedge.DifficultyStatisticsDisplay(true)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Statistics = new[]
|
||||
{
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.5f, 0.5f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.8f, 0.8f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.7f, 0.7f, 1f),
|
||||
}
|
||||
});
|
||||
|
||||
AddAssert("statistics visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
|
||||
AddAssert("tiny statistics hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
|
||||
|
||||
AddStep("set too many statistics", () => display.Statistics = new[]
|
||||
{
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.7f, 0.7f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.4f, 0.8f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 4", 0.3f, 0.3f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 5", 0.8f, 0.8f, 1f),
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 6", 0.5f, 0.5f, 1f),
|
||||
});
|
||||
|
||||
AddAssert("statistics still visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
|
||||
AddAssert("tiny statistics still hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
|
||||
|
||||
AddStep("set less statistics", () => display.Statistics = new[]
|
||||
{
|
||||
new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f),
|
||||
});
|
||||
|
||||
AddAssert("statistics still visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
|
||||
AddAssert("tiny statistics still hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@@ -14,6 +16,9 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneFPSCounter : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
@@ -41,6 +46,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
},
|
||||
};
|
||||
});
|
||||
AddToggleStep("toggle show", b => config.SetValue(OsuSetting.ShowFpsDisplay, b));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
/// </summary>
|
||||
public partial class StarRatingDisplay : CompositeDrawable, IHasCurrentValue<StarDifficulty>
|
||||
{
|
||||
public const double TRANSFORM_DURATION = 750;
|
||||
|
||||
private readonly bool animated;
|
||||
private readonly Box background;
|
||||
private readonly SpriteIcon starIcon;
|
||||
@@ -36,6 +38,12 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty colour currently displayed.
|
||||
/// Can be used to have other components match the spectrum animation.
|
||||
/// </summary>
|
||||
public Color4 DisplayedDifficultyColour => background.Colour;
|
||||
|
||||
private readonly Bindable<double> displayedStars = new BindableDouble();
|
||||
|
||||
/// <summary>
|
||||
@@ -139,7 +147,7 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
Current.BindValueChanged(c =>
|
||||
{
|
||||
if (animated)
|
||||
this.TransformBindableTo(displayedStars, c.NewValue.Stars, 750, Easing.OutQuint);
|
||||
this.TransformBindableTo(displayedStars, c.NewValue.Stars, TRANSFORM_DURATION, Easing.OutQuint);
|
||||
else
|
||||
displayedStars.Value = c.NewValue.Stars;
|
||||
});
|
||||
@@ -152,8 +160,8 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
|
||||
background.Colour = colours.ForStarDifficulty(s.NewValue);
|
||||
|
||||
starIcon.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47");
|
||||
starsText.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f);
|
||||
starIcon.Colour = s.NewValue >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47");
|
||||
starsText.Colour = s.NewValue >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ namespace osu.Game.Graphics
|
||||
public static Color4 Gray(float amt) => new Color4(amt, amt, amt, 1f);
|
||||
public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum star rating colour which can be distinguished against a black background.
|
||||
/// </summary>
|
||||
public const float STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF = 6.5f;
|
||||
|
||||
public static readonly (float, Color4)[] STAR_DIFFICULTY_SPECTRUM =
|
||||
{
|
||||
(0.1f, Color4Extensions.FromHex("aaaaaa")),
|
||||
|
||||
@@ -44,7 +44,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
AutoSizeAxes = Axes.Both,
|
||||
TextAnchor = Anchor.TopRight,
|
||||
Margin = new MarginPadding { Left = 5, Vertical = 10 },
|
||||
Text = string.Join('\n', gameHost.Threads.Select(t => t.Name))
|
||||
Text = string.Join('\n', gameHost.Threads.Select(t => t.Name)),
|
||||
ParagraphSpacing = 0,
|
||||
},
|
||||
textFlow = new OsuTextFlowContainer(cp =>
|
||||
{
|
||||
@@ -56,6 +57,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Margin = new MarginPadding { Left = 35, Right = 10, Vertical = 10 },
|
||||
AutoSizeAxes = Axes.Y,
|
||||
TextAnchor = Anchor.TopRight,
|
||||
ParagraphSpacing = 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
public IBindable<APIUser> LocalUser => localUser;
|
||||
public IBindableList<APIRelation> Friends => friends;
|
||||
public IBindableList<APIRelation> Blocks => blocks;
|
||||
|
||||
public INotificationsClient NotificationsClient { get; }
|
||||
|
||||
@@ -66,6 +67,7 @@ namespace osu.Game.Online.API
|
||||
private Bindable<APIUser> localUser { get; } = new Bindable<APIUser>(createGuestUser());
|
||||
|
||||
private BindableList<APIRelation> friends { get; } = new BindableList<APIRelation>();
|
||||
private BindableList<APIRelation> blocks { get; } = new BindableList<APIRelation>();
|
||||
|
||||
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
|
||||
|
||||
@@ -638,6 +640,35 @@ namespace osu.Game.Online.API
|
||||
Queue(friendsReq);
|
||||
}
|
||||
|
||||
public void UpdateLocalBlocks()
|
||||
{
|
||||
if (!IsLoggedIn)
|
||||
return;
|
||||
|
||||
var blocksReq = new GetBlocksRequest();
|
||||
blocksReq.Failure += ex =>
|
||||
{
|
||||
if (ex is not WebRequestFlushedException)
|
||||
state.Value = APIState.Failing;
|
||||
};
|
||||
blocksReq.Success += res =>
|
||||
{
|
||||
var existingBlocks = blocks.Select(f => f.TargetID).ToHashSet();
|
||||
var updatedBlocks = res.Select(f => f.TargetID).ToHashSet();
|
||||
|
||||
// Add new blocked users to local list.
|
||||
blocks.AddRange(res.Where(r => !existingBlocks.Contains(r.TargetID)));
|
||||
|
||||
// Remove non-blocked users from local list.
|
||||
blocks.RemoveAll(b => !updatedBlocks.Contains(b.TargetID));
|
||||
|
||||
// Remove friends who got blocked since last check.
|
||||
friends.RemoveAll(f => updatedBlocks.Contains(f.TargetID));
|
||||
};
|
||||
|
||||
Queue(blocksReq);
|
||||
}
|
||||
|
||||
private static APIUser createGuestUser() => new GuestUser();
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace osu.Game.Online.API
|
||||
});
|
||||
|
||||
public BindableList<APIRelation> Friends { get; } = new BindableList<APIRelation>();
|
||||
public BindableList<APIRelation> Blocks { get; } = new BindableList<APIRelation>();
|
||||
|
||||
public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient();
|
||||
INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient;
|
||||
@@ -180,6 +181,10 @@ namespace osu.Game.Online.API
|
||||
{
|
||||
}
|
||||
|
||||
public void UpdateLocalBlocks()
|
||||
{
|
||||
}
|
||||
|
||||
public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
|
||||
|
||||
public IChatClient GetChatClient() => new TestChatClientConnector(this);
|
||||
@@ -194,6 +199,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
IBindable<APIUser> IAPIProvider.LocalUser => LocalUser;
|
||||
IBindableList<APIRelation> IAPIProvider.Friends => Friends;
|
||||
IBindableList<APIRelation> IAPIProvider.Blocks => Blocks;
|
||||
|
||||
/// <summary>
|
||||
/// Skip 2FA requirement for next login.
|
||||
|
||||
@@ -23,6 +23,11 @@ namespace osu.Game.Online.API
|
||||
/// </summary>
|
||||
IBindableList<APIRelation> Friends { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The users blocked by the local user.
|
||||
/// </summary>
|
||||
IBindableList<APIRelation> Blocks { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The language supplied by this provider to API requests.
|
||||
/// </summary>
|
||||
@@ -118,6 +123,11 @@ namespace osu.Game.Online.API
|
||||
/// </summary>
|
||||
void UpdateLocalFriends();
|
||||
|
||||
/// <summary>
|
||||
/// Update the list of users blocked by the current user.
|
||||
/// </summary>
|
||||
void UpdateLocalBlocks();
|
||||
|
||||
/// <summary>
|
||||
/// Schedule a callback to run on the update thread.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class BlockUserRequest : APIRequest
|
||||
{
|
||||
public readonly int TargetId;
|
||||
|
||||
public BlockUserRequest(int targetId)
|
||||
{
|
||||
TargetId = targetId;
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var req = base.CreateWebRequest();
|
||||
|
||||
req.Method = HttpMethod.Post;
|
||||
req.AddParameter("target", TargetId.ToString(), RequestParameterType.Query);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
protected override string Target => @"blocks";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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.Collections.Generic;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class GetBlocksRequest : APIRequest<List<APIRelation>>
|
||||
{
|
||||
protected override string Target => @"blocks";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class UnblockUserRequest : APIRequest
|
||||
{
|
||||
public readonly int TargetId;
|
||||
|
||||
public UnblockUserRequest(int targetId)
|
||||
{
|
||||
TargetId = targetId;
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var req = base.CreateWebRequest();
|
||||
req.Method = HttpMethod.Delete;
|
||||
return req;
|
||||
}
|
||||
|
||||
protected override string Target => @$"blocks/{TargetId}";
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
@@ -30,8 +30,6 @@ namespace osu.Game.Online.Leaderboards
|
||||
public LeaderboardCriteria? CurrentCriteria { get; private set; }
|
||||
|
||||
private IDisposable? localScoreSubscription;
|
||||
private TaskCompletionSource<LeaderboardScores?>? localFetchCompletionSource;
|
||||
private TaskCompletionSource<LeaderboardScores?>? lastFetchCompletionSource;
|
||||
private GetScoresRequest? inFlightOnlineRequest;
|
||||
|
||||
[Resolved]
|
||||
@@ -43,55 +41,70 @@ namespace osu.Game.Online.Leaderboards
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
public Task<LeaderboardScores?> FetchWithCriteriaAsync(LeaderboardCriteria newCriteria)
|
||||
/// <summary>
|
||||
/// Fetch leaderboard content with the new criteria specified in the background.
|
||||
/// On completion, <see cref="Scores"/> will be updated with the results from this call (unless a more recent call with a different criteria has completed).
|
||||
/// </summary>
|
||||
public void FetchWithCriteria(LeaderboardCriteria newCriteria, bool forceRefresh = false)
|
||||
{
|
||||
if (CurrentCriteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false)
|
||||
return lastFetchCompletionSource?.Task ?? Task.FromResult(Scores.Value);
|
||||
if (!forceRefresh && CurrentCriteria?.Equals(newCriteria) == true && scores.Value?.FailState == null)
|
||||
return;
|
||||
|
||||
CurrentCriteria = newCriteria;
|
||||
localScoreSubscription?.Dispose();
|
||||
inFlightOnlineRequest?.Cancel();
|
||||
lastFetchCompletionSource?.TrySetCanceled();
|
||||
scores.Value = null;
|
||||
|
||||
if (newCriteria.Beatmap == null || newCriteria.Ruleset == null)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoneSelected));
|
||||
{
|
||||
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoneSelected);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (newCriteria.Scope)
|
||||
{
|
||||
case BeatmapLeaderboardScope.Local:
|
||||
{
|
||||
// this task completion source will be marked completed in the `localScoresChanged()` below.
|
||||
// yes it's twisty, but such are the costs of trying to reconcile data-push / subscription and data-pull / explicit fetch flows.
|
||||
lastFetchCompletionSource = localFetchCompletionSource = new TaskCompletionSource<LeaderboardScores?>();
|
||||
localScoreSubscription = realm.RegisterForNotifications(r =>
|
||||
r.All<ScoreInfo>().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0"
|
||||
+ $" AND {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}"
|
||||
+ $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1"
|
||||
+ $" AND {nameof(ScoreInfo.DeletePending)} == false"
|
||||
, newCriteria.Beatmap.ID, newCriteria.Ruleset.ShortName), localScoresChanged);
|
||||
return localFetchCompletionSource.Task;
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
if (!api.IsLoggedIn)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotLoggedIn));
|
||||
{
|
||||
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotLoggedIn);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newCriteria.Ruleset.IsLegacyRuleset())
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.RulesetUnavailable));
|
||||
{
|
||||
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.RulesetUnavailable);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newCriteria.Beatmap.OnlineID <= 0 || newCriteria.Beatmap.Status <= BeatmapOnlineStatus.Pending)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.BeatmapUnavailable));
|
||||
{
|
||||
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.BeatmapUnavailable);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((newCriteria.Scope.RequiresSupporter(newCriteria.ExactMods != null)) && !api.LocalUser.Value.IsSupporter)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotSupporter));
|
||||
{
|
||||
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotSupporter);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newCriteria.Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null)
|
||||
return Task.FromResult<LeaderboardScores?>(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoTeam));
|
||||
|
||||
var onlineFetchCompletionSource = new TaskCompletionSource<LeaderboardScores?>();
|
||||
lastFetchCompletionSource = onlineFetchCompletionSource;
|
||||
{
|
||||
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoTeam);
|
||||
return;
|
||||
}
|
||||
|
||||
IReadOnlyList<Mod>? requestMods = null;
|
||||
|
||||
@@ -116,12 +129,17 @@ namespace osu.Game.Online.Leaderboards
|
||||
response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap)
|
||||
);
|
||||
inFlightOnlineRequest = null;
|
||||
if (onlineFetchCompletionSource.TrySetResult(result))
|
||||
scores.Value = result;
|
||||
scores.Value = result;
|
||||
};
|
||||
newRequest.Failure += _ => onlineFetchCompletionSource.TrySetResult(LeaderboardScores.Failure(LeaderboardFailState.NetworkFailure));
|
||||
newRequest.Failure += ex =>
|
||||
{
|
||||
Logger.Log($@"Failed to fetch leaderboards when displaying results: {ex}", LoggingTarget.Network);
|
||||
if (ex is not OperationCanceledException)
|
||||
scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NetworkFailure);
|
||||
};
|
||||
|
||||
api.Queue(inFlightOnlineRequest = newRequest);
|
||||
return onlineFetchCompletionSource.Task;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,12 +175,6 @@ namespace osu.Game.Online.Leaderboards
|
||||
newScores = newScores.Detach().OrderByTotalScore();
|
||||
|
||||
scores.Value = LeaderboardScores.Success(newScores.ToArray(), null);
|
||||
|
||||
if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource)
|
||||
{
|
||||
localFetchCompletionSource.SetResult(scores.Value);
|
||||
localFetchCompletionSource = lastFetchCompletionSource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-6
@@ -801,12 +801,7 @@ namespace osu.Game
|
||||
var newLeaderboard = currentLeaderboard != null
|
||||
? currentLeaderboard with { Beatmap = databasedBeatmap, Ruleset = databasedScore.ScoreInfo.Ruleset }
|
||||
: new LeaderboardCriteria(databasedBeatmap, databasedScore.ScoreInfo.Ruleset, BeatmapLeaderboardScope.Global, null);
|
||||
LeaderboardManager.FetchWithCriteriaAsync(newLeaderboard)
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if (t.Exception != null)
|
||||
Logger.Log($@"Failed to fetch leaderboards when displaying results: {t.Exception}", LoggingTarget.Network);
|
||||
});
|
||||
LeaderboardManager.FetchWithCriteria(newLeaderboard);
|
||||
}
|
||||
|
||||
switch (presentType)
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public partial class AdjustedAttributesTooltip : VisibilityContainer, ITooltip<AdjustedAttributesTooltip.Data?>
|
||||
{
|
||||
private readonly OverlayColourProvider? colourProvider;
|
||||
private FillFlowContainer attributesFillFlow = null!;
|
||||
|
||||
private Container content = null!;
|
||||
@@ -27,6 +28,11 @@ namespace osu.Game.Overlays.Mods
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
public AdjustedAttributesTooltip(OverlayColourProvider? colourProvider = null)
|
||||
{
|
||||
this.colourProvider = colourProvider;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@@ -45,7 +51,7 @@ namespace osu.Game.Overlays.Mods
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colours.Gray3,
|
||||
Colour = colourProvider?.Background4 ?? colours.Gray3,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
|
||||
@@ -44,6 +44,8 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
OsuTextFlowContainer label;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
content = new Container
|
||||
@@ -69,12 +71,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
|
||||
label = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
// can't use this because osu-web does weird stuff with \\n.
|
||||
// Text = UsersStrings.ShowDailyChallengeTitle.,
|
||||
Text = "Daily\nChallenge",
|
||||
Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f },
|
||||
},
|
||||
new Container
|
||||
@@ -129,6 +128,10 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// can't use this because osu-web does weird stuff with \\n.
|
||||
// Text = UsersStrings.ShowDailyChallengeTitle.,
|
||||
label.AddParagraph("Daily\nChallenge");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
||||
@@ -80,6 +80,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(0.8f),
|
||||
Icon = FontAwesome.Solid.Bars,
|
||||
Enabled = { BindTarget = Enabled },
|
||||
Action = () => freeModSelectOverlay.ToggleVisibility()
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@@ -431,14 +430,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
/// </summary>
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
bool newIsRoomJoined = client.Room != null;
|
||||
bool wasRoomJoined = isRoomJoined;
|
||||
isRoomJoined = client.Room != null;
|
||||
|
||||
if (newIsRoomJoined)
|
||||
// Creating a room.
|
||||
if (!wasRoomJoined && !isRoomJoined)
|
||||
{
|
||||
roomContent.Hide();
|
||||
settingsOverlay.Show();
|
||||
}
|
||||
|
||||
// Joining a room.
|
||||
if (!wasRoomJoined && isRoomJoined)
|
||||
{
|
||||
roomContent.Show();
|
||||
settingsOverlay.Hide();
|
||||
}
|
||||
else if (isRoomJoined)
|
||||
|
||||
// Leaving a room.
|
||||
if (wasRoomJoined && !isRoomJoined)
|
||||
{
|
||||
Logger.Log($"{this} exiting due to loss of room or connection");
|
||||
|
||||
@@ -447,17 +457,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
else
|
||||
ValidForResume = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Assert(!isRoomJoined && !newIsRoomJoined);
|
||||
|
||||
// A new room is being created.
|
||||
// The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed.
|
||||
roomContent.Hide();
|
||||
settingsOverlay.Show();
|
||||
}
|
||||
|
||||
isRoomJoined = newIsRoomJoined;
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Bindable<LeaderboardScores?> fetchedScores = new Bindable<LeaderboardScores?>();
|
||||
private readonly IBindable<LeaderboardScores?> fetchedScores = new Bindable<LeaderboardScores?>();
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
@@ -82,9 +82,10 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
if (filterMods)
|
||||
RefetchScores();
|
||||
};
|
||||
((IBindable<LeaderboardScores?>)fetchedScores).BindTo(leaderboardManager.Scores);
|
||||
}
|
||||
|
||||
private bool initialFetchComplete;
|
||||
|
||||
protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;
|
||||
|
||||
protected override APIRequest? FetchScores(CancellationToken cancellationToken)
|
||||
@@ -92,30 +93,38 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
var fetchBeatmapInfo = BeatmapInfo;
|
||||
var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo?.Ruleset;
|
||||
|
||||
leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null))
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if (t.Exception != null && !t.IsCanceled)
|
||||
{
|
||||
Schedule(() => SetErrorState(LeaderboardState.NetworkFailure));
|
||||
return;
|
||||
}
|
||||
// Without this check, an initial fetch will be performed and clear global cache.
|
||||
if (fetchBeatmapInfo == null)
|
||||
return null;
|
||||
|
||||
fetchedScores.UnbindEvents();
|
||||
fetchedScores.BindValueChanged(scores =>
|
||||
{
|
||||
if (scores.NewValue == null) return;
|
||||
// For now, we forcefully refresh to keep things simple.
|
||||
// In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios
|
||||
// (like returning from gameplay after setting a new score, returning to song select after main menu).
|
||||
leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null), forceRefresh: true);
|
||||
|
||||
if (scores.NewValue.FailState == null)
|
||||
Schedule(() => SetScores(scores.NewValue.TopScores, scores.NewValue.UserScore));
|
||||
else
|
||||
Schedule(() => SetErrorState((LeaderboardState)scores.NewValue.FailState));
|
||||
}, true);
|
||||
}, cancellationToken);
|
||||
if (!initialFetchComplete)
|
||||
{
|
||||
// only bind this after the first fetch to avoid reading stale scores.
|
||||
fetchedScores.BindTo(leaderboardManager.Scores);
|
||||
fetchedScores.BindValueChanged(_ => updateScores(), true);
|
||||
initialFetchComplete = true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void updateScores()
|
||||
{
|
||||
var scores = fetchedScores.Value;
|
||||
|
||||
if (scores == null) return;
|
||||
|
||||
if (scores.FailState == null)
|
||||
Schedule(() => SetScores(scores.TopScores, scores.UserScore));
|
||||
else
|
||||
Schedule(() => SetErrorState((LeaderboardState)scores.FailState));
|
||||
}
|
||||
|
||||
protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope, Scope != BeatmapLeaderboardScope.Friend)
|
||||
{
|
||||
Action = () => ScoreSelected?.Invoke(model)
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
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.Online.Chat;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapMetadataWedge : VisibilityContainer
|
||||
{
|
||||
private MetadataDisplay creator = null!;
|
||||
private MetadataDisplay source = null!;
|
||||
private MetadataDisplay genre = null!;
|
||||
private MetadataDisplay language = null!;
|
||||
private MetadataDisplay tag = null!;
|
||||
private MetadataDisplay submitted = null!;
|
||||
private MetadataDisplay ranked = null!;
|
||||
|
||||
private Drawable ratingsWedge = null!;
|
||||
private SuccessRateDisplay successRateDisplay = null!;
|
||||
private UserRatingDisplay userRatingDisplay = null!;
|
||||
private RatingSpreadDisplay ratingSpreadDisplay = null!;
|
||||
|
||||
private Drawable failRetryWedge = null!;
|
||||
private FailRetryDisplay failRetryDisplay = null!;
|
||||
|
||||
protected override bool StartHidden => true;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private IBindable<APIState> apiState = null!;
|
||||
|
||||
[Resolved]
|
||||
private ILinkHandler? linkHandler { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private SongSelect? songSelect { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Padding = new MarginPadding { Top = 4f };
|
||||
|
||||
Width = 0.9f;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 4f),
|
||||
Shear = OsuGame.SHEAR,
|
||||
Children = new[]
|
||||
{
|
||||
new ShearAligningWrapper(new Container
|
||||
{
|
||||
CornerRadius = 10,
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new WedgeBackground(),
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 35, Vertical = 16 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 10f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(),
|
||||
new Dimension(),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 10f),
|
||||
Children = new[]
|
||||
{
|
||||
creator = new MetadataDisplay("Creator"),
|
||||
genre = new MetadataDisplay("Genre"),
|
||||
},
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 10f),
|
||||
Children = new[]
|
||||
{
|
||||
source = new MetadataDisplay("Source"),
|
||||
language = new MetadataDisplay("Language"),
|
||||
},
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 10f),
|
||||
Children = new[]
|
||||
{
|
||||
submitted = new MetadataDisplay("Submitted"),
|
||||
ranked = new MetadataDisplay("Ranked"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tag = new MetadataDisplay("Tags"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
new ShearAligningWrapper(ratingsWedge = new Container
|
||||
{
|
||||
Alpha = 0f,
|
||||
CornerRadius = 10,
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new WedgeBackground(),
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 10),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 10),
|
||||
new Dimension(),
|
||||
},
|
||||
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 40f, Vertical = 16 },
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
successRateDisplay = new SuccessRateDisplay(),
|
||||
Empty(),
|
||||
userRatingDisplay = new UserRatingDisplay(),
|
||||
Empty(),
|
||||
ratingSpreadDisplay = new RatingSpreadDisplay(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}),
|
||||
new ShearAligningWrapper(failRetryWedge = new Container
|
||||
{
|
||||
Alpha = 0f,
|
||||
CornerRadius = 10,
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new WedgeBackground(),
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 40f, Vertical = 16 },
|
||||
Child = failRetryDisplay = new FailRetryDisplay(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
beatmap.BindValueChanged(_ => updateDisplay());
|
||||
|
||||
apiState = api.State.GetBoundCopy();
|
||||
apiState.BindValueChanged(_ => Scheduler.AddOnce(updateDisplay), true);
|
||||
}
|
||||
|
||||
private const double transition_duration = 300;
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(transition_duration, Easing.OutQuint)
|
||||
.MoveToX(0, transition_duration, Easing.OutQuint);
|
||||
|
||||
updateSubWedgeVisibility();
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(transition_duration, Easing.OutQuint)
|
||||
.MoveToX(-100, transition_duration, Easing.OutQuint);
|
||||
|
||||
updateSubWedgeVisibility();
|
||||
}
|
||||
|
||||
private void updateSubWedgeVisibility()
|
||||
{
|
||||
// We could consider hiding individual wedges based on zero data in the future.
|
||||
// Needs some experimentation on what looks good.
|
||||
|
||||
if (State.Value == Visibility.Visible && currentOnlineBeatmapSet != null)
|
||||
{
|
||||
ratingsWedge.FadeIn(transition_duration, Easing.OutQuint)
|
||||
.MoveToX(0, transition_duration, Easing.OutQuint);
|
||||
|
||||
failRetryWedge.Delay(100)
|
||||
.FadeIn(transition_duration, Easing.OutQuint)
|
||||
.MoveToX(0, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
ratingsWedge.FadeOut(transition_duration, Easing.OutQuint)
|
||||
.MoveToX(-50, transition_duration, Easing.OutQuint);
|
||||
|
||||
failRetryWedge.Delay(100)
|
||||
.FadeOut(transition_duration, Easing.OutQuint)
|
||||
.MoveToX(-50, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
var metadata = beatmap.Value.Metadata;
|
||||
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
|
||||
|
||||
creator.Data = (metadata.Author.Username, () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, metadata.Author)));
|
||||
|
||||
if (!string.IsNullOrEmpty(metadata.Source))
|
||||
source.Data = (metadata.Source, () => songSelect?.Search(metadata.Source));
|
||||
else
|
||||
source.Data = ("-", null);
|
||||
|
||||
tag.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t));
|
||||
submitted.Date = beatmapSetInfo.DateSubmitted;
|
||||
ranked.Date = beatmapSetInfo.DateRanked;
|
||||
|
||||
if (currentOnlineBeatmapSet == null || currentOnlineBeatmapSet.OnlineID != beatmapSetInfo.OnlineID)
|
||||
refetchBeatmapSet();
|
||||
|
||||
updateOnlineDisplay();
|
||||
}
|
||||
|
||||
private APIBeatmapSet? currentOnlineBeatmapSet;
|
||||
private GetBeatmapSetRequest? currentRequest;
|
||||
|
||||
private void refetchBeatmapSet()
|
||||
{
|
||||
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
|
||||
|
||||
currentRequest?.Cancel();
|
||||
currentRequest = null;
|
||||
currentOnlineBeatmapSet = null;
|
||||
|
||||
if (beatmapSetInfo.OnlineID >= 1)
|
||||
{
|
||||
// todo: consider introducing a BeatmapSetLookupCache for caching benefits.
|
||||
currentRequest = new GetBeatmapSetRequest(beatmapSetInfo.OnlineID);
|
||||
currentRequest.Failure += _ => updateOnlineDisplay();
|
||||
currentRequest.Success += s =>
|
||||
{
|
||||
currentOnlineBeatmapSet = s;
|
||||
updateOnlineDisplay();
|
||||
};
|
||||
|
||||
api.Queue(currentRequest);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateOnlineDisplay()
|
||||
{
|
||||
if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting)
|
||||
{
|
||||
genre.Data = null;
|
||||
language.Data = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentOnlineBeatmapSet == null)
|
||||
{
|
||||
genre.Data = ("-", null);
|
||||
language.Data = ("-", null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
|
||||
var onlineBeatmapSet = currentOnlineBeatmapSet;
|
||||
var onlineBeatmap = onlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID);
|
||||
|
||||
genre.Data = (onlineBeatmapSet.Genre.Name, () => songSelect?.Search(onlineBeatmapSet.Genre.Name));
|
||||
language.Data = (onlineBeatmapSet.Language.Name, () => songSelect?.Search(onlineBeatmapSet.Language.Name));
|
||||
|
||||
if (onlineBeatmap != null)
|
||||
{
|
||||
userRatingDisplay.Data = onlineBeatmapSet.Ratings;
|
||||
ratingSpreadDisplay.Data = onlineBeatmapSet.Ratings;
|
||||
successRateDisplay.Data = (onlineBeatmap.PassCount, onlineBeatmap.PlayCount);
|
||||
failRetryDisplay.Data = onlineBeatmap.FailTimes ?? new APIFailTimes();
|
||||
}
|
||||
}
|
||||
|
||||
updateSubWedgeVisibility();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapMetadataWedge
|
||||
{
|
||||
private partial class FailRetryDisplay : CompositeDrawable
|
||||
{
|
||||
private readonly GraphDrawable retriesGraph;
|
||||
private readonly GraphDrawable failsGraph;
|
||||
|
||||
public APIFailTimes Data
|
||||
{
|
||||
set
|
||||
{
|
||||
int[] retries = value.Retries ?? Array.Empty<int>();
|
||||
int[] fails = value.Fails ?? Array.Empty<int>();
|
||||
int[] total = retries.Zip(fails, (r, f) => r + f).ToArray();
|
||||
|
||||
int maximum = total.DefaultIfEmpty(0).Max();
|
||||
|
||||
retriesGraph.Data = total.Select(r => maximum == 0 ? 0 : (float)r / maximum).ToArray();
|
||||
failsGraph.Data = fails.Select(r => maximum == 0 ? 0 : (float)r / maximum).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public FailRetryDisplay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 4f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = BeatmapsetsStrings.ShowInfoPointsOfFailure,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
Margin = new MarginPadding { Bottom = 4f },
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 65f,
|
||||
Children = new[]
|
||||
{
|
||||
retriesGraph = new GraphDrawable { RelativeSizeAxes = Axes.Both, Y = -1f },
|
||||
failsGraph = new GraphDrawable { RelativeSizeAxes = Axes.Both },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
retriesGraph.Colour = colours.Orange1;
|
||||
failsGraph.Colour = colours.DarkOrange2;
|
||||
}
|
||||
|
||||
private partial class GraphDrawable : Drawable
|
||||
{
|
||||
private readonly float[] displayedData = new float[100];
|
||||
|
||||
private float[] data = new float[100];
|
||||
|
||||
public float[] Data
|
||||
{
|
||||
get => data;
|
||||
set
|
||||
{
|
||||
data = value;
|
||||
Invalidate(Invalidation.DrawNode);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
bool changed = false;
|
||||
|
||||
for (int i = 0; i < displayedData.Length; i++)
|
||||
{
|
||||
float before = displayedData[i];
|
||||
float value = data.ElementAtOrDefault(i);
|
||||
displayedData[i] = (float)Interpolation.DampContinuously(displayedData[i], value, 40, Time.Elapsed);
|
||||
changed |= displayedData[i] != before;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
Invalidate(Invalidation.DrawNode);
|
||||
}
|
||||
|
||||
protected override DrawNode CreateDrawNode() => new GraphDrawNode(this);
|
||||
|
||||
// todo: consider integrating this with BarGraph
|
||||
// this is different from BarGraph since this displays each bar with corner radii applied.
|
||||
private class GraphDrawNode : DrawNode
|
||||
{
|
||||
private readonly GraphDrawable source;
|
||||
|
||||
private Vector2 drawSize;
|
||||
private float[] displayedData = null!;
|
||||
|
||||
public GraphDrawNode(GraphDrawable source)
|
||||
: base(source)
|
||||
{
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public override void ApplyState()
|
||||
{
|
||||
base.ApplyState();
|
||||
|
||||
drawSize = source.DrawSize;
|
||||
displayedData = source.displayedData;
|
||||
}
|
||||
|
||||
protected override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
const float spacing_constant = 1.5f;
|
||||
|
||||
float position = 0;
|
||||
float barWidth = drawSize.X / displayedData.Length / spacing_constant;
|
||||
|
||||
float totalSpacing = drawSize.X - barWidth * displayedData.Length;
|
||||
float spacing = totalSpacing / (displayedData.Length - 1);
|
||||
|
||||
for (int i = 0; i < displayedData.Length; i++)
|
||||
{
|
||||
float barHeight = MathF.Max(drawSize.Y * displayedData[i], barWidth);
|
||||
|
||||
drawBar(renderer, position, barWidth, barHeight);
|
||||
|
||||
position += barWidth + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawBar(IRenderer renderer, float position, float width, float height)
|
||||
{
|
||||
float cornerRadius = width / 2f;
|
||||
|
||||
Vector3 scale = DrawInfo.MatrixInverse.ExtractScale();
|
||||
float blendRange = (scale.X + scale.Y) / 2;
|
||||
|
||||
RectangleF drawRectangle = new RectangleF(new Vector2(position, drawSize.Y - height), new Vector2(width, height));
|
||||
Quad screenSpaceDrawQuad = Quad.FromRectangle(drawRectangle) * DrawInfo.Matrix;
|
||||
|
||||
renderer.PushMaskingInfo(new MaskingInfo
|
||||
{
|
||||
ScreenSpaceAABB = screenSpaceDrawQuad.AABB,
|
||||
MaskingRect = drawRectangle.Normalize(),
|
||||
ConservativeScreenSpaceQuad = screenSpaceDrawQuad,
|
||||
ToMaskingSpace = DrawInfo.MatrixInverse,
|
||||
CornerRadius = cornerRadius,
|
||||
CornerExponent = 2f,
|
||||
// We are setting the linear blend range to the approximate size of a _pixel_ here.
|
||||
// This results in the optimal trade-off between crispness and smoothness of the
|
||||
// edges of the masked region according to sampling theory.
|
||||
BlendRange = blendRange,
|
||||
AlphaExponent = 1,
|
||||
});
|
||||
|
||||
renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour);
|
||||
renderer.PopMaskingInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
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.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapMetadataWedge
|
||||
{
|
||||
private partial class MetadataDisplay : FillFlowContainer
|
||||
{
|
||||
private readonly OsuSpriteText labelText;
|
||||
private readonly OsuSpriteText contentText;
|
||||
private readonly OsuSpriteText contentLinkText;
|
||||
private readonly OsuHoverContainer contentLink;
|
||||
private readonly DrawableDate contentDate;
|
||||
private readonly TagsLine contentTags;
|
||||
private readonly LoadingSpinner contentLoading;
|
||||
|
||||
private (LocalisableString value, Action? linkAction)? data;
|
||||
|
||||
public (LocalisableString value, Action? linkAction)? Data
|
||||
{
|
||||
get => data;
|
||||
set
|
||||
{
|
||||
data = value;
|
||||
|
||||
if (value?.linkAction != null)
|
||||
setLink(value.Value.value, value.Value.linkAction);
|
||||
else if (value.HasValue)
|
||||
setText(value.Value.value);
|
||||
else
|
||||
setLoading();
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset? Date
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value != null)
|
||||
setDate(value.Value);
|
||||
else
|
||||
setText("-");
|
||||
}
|
||||
}
|
||||
|
||||
public (string[] tags, Action<string> linkAction) Tags
|
||||
{
|
||||
set => setTags(value.tags, value.linkAction);
|
||||
}
|
||||
|
||||
public MetadataDisplay(LocalisableString label)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Padding = new MarginPadding { Right = 10 };
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
labelText = new OsuSpriteText
|
||||
{
|
||||
Text = label,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = OsuFont.Style.Caption1.Size,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
contentText = new TruncatingSpriteText
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Font = OsuFont.Style.Caption1,
|
||||
},
|
||||
contentLink = new OsuHoverContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = contentLinkText = new TruncatingSpriteText
|
||||
{
|
||||
Font = OsuFont.Style.Caption1,
|
||||
},
|
||||
},
|
||||
contentDate = new DrawableDate(default, OsuFont.Style.Caption1.Size, false),
|
||||
contentTags = new TagsLine(),
|
||||
contentLoading = new LoadingSpinner
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Size = new Vector2(10),
|
||||
Margin = new MarginPadding { Top = 3f },
|
||||
State = { Value = Visibility.Visible },
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
labelText.Colour = colourProvider.Content1;
|
||||
contentText.Colour = colourProvider.Content2;
|
||||
contentLink.IdleColour = colourProvider.Light2;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
contentLinkText.MaxWidth = ChildSize.X;
|
||||
}
|
||||
|
||||
private void clear()
|
||||
{
|
||||
contentText.Text = string.Empty;
|
||||
contentLinkText.Text = string.Empty;
|
||||
contentDate.Hide();
|
||||
contentTags.Tags = Array.Empty<string>();
|
||||
contentLoading.Hide();
|
||||
}
|
||||
|
||||
private void setText(LocalisableString text)
|
||||
{
|
||||
clear();
|
||||
|
||||
contentText.Text = text;
|
||||
}
|
||||
|
||||
private void setLink(LocalisableString text, Action action) => Schedule(() =>
|
||||
{
|
||||
clear();
|
||||
|
||||
contentLinkText.Text = text;
|
||||
contentLink.Action = action;
|
||||
});
|
||||
|
||||
private void setDate(DateTimeOffset date)
|
||||
{
|
||||
clear();
|
||||
|
||||
contentDate.Show();
|
||||
contentDate.Date = date;
|
||||
}
|
||||
|
||||
private void setTags(string[] tags, Action<string> link)
|
||||
{
|
||||
clear();
|
||||
|
||||
contentTags.Tags = tags;
|
||||
contentTags.Action = link;
|
||||
}
|
||||
|
||||
private void setLoading()
|
||||
{
|
||||
clear();
|
||||
|
||||
contentLoading.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// 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.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapMetadataWedge
|
||||
{
|
||||
private partial class RatingSpreadDisplay : CompositeDrawable
|
||||
{
|
||||
private const float min_height = 4f;
|
||||
private const float max_height = 32f;
|
||||
|
||||
private const int rating_range = 10;
|
||||
|
||||
private readonly GraphBar[] graph;
|
||||
|
||||
public int[] Data
|
||||
{
|
||||
set
|
||||
{
|
||||
if (!value.Any())
|
||||
{
|
||||
foreach (var bar in graph)
|
||||
bar.ResizeHeightTo(min_height, 300, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
var usableRange = value.Skip(1).Take(rating_range); // adjust for API returning weird empty data at 0.
|
||||
int maxRating = usableRange.Max();
|
||||
|
||||
for (int i = 0; i < graph.Length; i++)
|
||||
graph[i].ResizeHeightTo(min_height + (max_height - min_height) * (maxRating == 0 ? 0 : usableRange.ElementAt(i) / (float)maxRating), 300, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RatingSpreadDisplay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
graph = Enumerable.Range(0, rating_range).Select(_ => new GraphBar()).ToArray();
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 1f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = BeatmapsetsStrings.ShowStatsRatingSpread,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, max_height) },
|
||||
ColumnDimensions = graph.SkipLast(1).Select(_ => new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 1f),
|
||||
}).SelectMany(d => d).Append(new Dimension()).ToArray(),
|
||||
Content = new[]
|
||||
{
|
||||
graph.SkipLast(1).Select(g => new[]
|
||||
{
|
||||
g,
|
||||
Empty()
|
||||
}).SelectMany(g => g).Append(graph[^1]).ToArray()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var left = Interpolation.ValueAt(i, colours.Blue4, colours.Blue0, 0, 10);
|
||||
var right = Interpolation.ValueAt(i + 1, colours.Blue4, colours.Blue0, 0, 10);
|
||||
graph[i].Colour = ColourInfo.GradientHorizontal(left, right);
|
||||
}
|
||||
}
|
||||
|
||||
private partial class GraphBar : CompositeDrawable
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Anchor = Anchor.BottomLeft;
|
||||
Origin = Anchor.BottomLeft;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
CornerRadius = 2f;
|
||||
Masking = true;
|
||||
|
||||
InternalChild = new Box { RelativeSizeAxes = Axes.Both };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapMetadataWedge
|
||||
{
|
||||
private partial class SuccessRateDisplay : CompositeDrawable, IHasTooltip
|
||||
{
|
||||
private readonly OsuSpriteText valueText;
|
||||
private readonly Circle backgroundBar;
|
||||
private readonly Circle valueBar;
|
||||
|
||||
private (int passes, int plays) data;
|
||||
|
||||
public (int passes, int plays) Data
|
||||
{
|
||||
get => data;
|
||||
set
|
||||
{
|
||||
data = value;
|
||||
|
||||
float ratio = value.plays == 0 ? 0 : (float)value.passes / value.plays;
|
||||
|
||||
valueText.Text = ratio.ToLocalisableString(@"0.##%");
|
||||
valueText.MoveToX(Math.Clamp(ratio, 0.05f, 0.95f), 300, Easing.OutQuint);
|
||||
valueBar.ResizeWidthTo(ratio, 300, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
public LocalisableString TooltipText => $"{data.passes:N0} / {data.plays:N0}";
|
||||
|
||||
public SuccessRateDisplay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = BeatmapsetsStrings.ShowInfoSuccessRate,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Top = 10f },
|
||||
Child = valueText = new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativePositionAxes = Axes.X,
|
||||
Font = OsuFont.Style.Caption1,
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new[]
|
||||
{
|
||||
backgroundBar = new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 4f,
|
||||
},
|
||||
valueBar = new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0f,
|
||||
Height = 4f,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, OverlayColourProvider colourProvider)
|
||||
{
|
||||
backgroundBar.Colour = colourProvider.Background6;
|
||||
valueBar.Colour = colours.Lime1;
|
||||
valueText.Colour = colourProvider.Content2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapMetadataWedge
|
||||
{
|
||||
private partial class TagsLine : FillFlowContainer
|
||||
{
|
||||
private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize);
|
||||
|
||||
private string[] tags = Array.Empty<string>();
|
||||
|
||||
private TagsOverflowButton? overflowButton;
|
||||
|
||||
public string[] Tags
|
||||
{
|
||||
get => tags;
|
||||
set
|
||||
{
|
||||
tags = value;
|
||||
updateTags();
|
||||
}
|
||||
}
|
||||
|
||||
public Action<string>? Action;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public TagsLine()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Direction = FillDirection.Horizontal;
|
||||
Spacing = new Vector2(4, 0);
|
||||
|
||||
AddLayout(drawSizeLayout);
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (!drawSizeLayout.IsValid)
|
||||
{
|
||||
updateLayout();
|
||||
drawSizeLayout.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLayout()
|
||||
{
|
||||
if (tags.Length == 0)
|
||||
return;
|
||||
|
||||
Debug.Assert(overflowButton != null);
|
||||
|
||||
float limit = DrawWidth - overflowButton.DrawWidth - 5;
|
||||
bool showOverflow = false;
|
||||
|
||||
foreach (var text in Children)
|
||||
{
|
||||
if (text.X + text.DrawWidth < limit)
|
||||
text.Show();
|
||||
else
|
||||
{
|
||||
showOverflow = true;
|
||||
text.AlwaysPresent = false;
|
||||
text.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
if (showOverflow)
|
||||
overflowButton.Show();
|
||||
else
|
||||
overflowButton.Hide();
|
||||
}
|
||||
|
||||
private void updateTags()
|
||||
{
|
||||
ChildrenEnumerable = tags.Select(t => new OsuHoverContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Action = () => Action?.Invoke(t),
|
||||
IdleColour = colourProvider.Light2,
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0f,
|
||||
Child = new OsuSpriteText
|
||||
{
|
||||
Text = t,
|
||||
Font = OsuFont.Style.Caption1,
|
||||
},
|
||||
});
|
||||
|
||||
Add(overflowButton = new TagsOverflowButton(tags)
|
||||
{
|
||||
Alpha = 0f,
|
||||
});
|
||||
|
||||
drawSizeLayout.Invalidate();
|
||||
}
|
||||
|
||||
private partial class TagsOverflowButton : CompositeDrawable, IHasPopover, IHasLineBaseHeight
|
||||
{
|
||||
private readonly string[] tags;
|
||||
|
||||
private Box box = null!;
|
||||
private OsuSpriteText text = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private SongSelect? songSelect { get; set; }
|
||||
|
||||
public float LineBaseHeight => text.LineBaseHeight;
|
||||
|
||||
public TagsOverflowButton(string[] tags)
|
||||
{
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size = new Vector2(OsuFont.Style.Caption1.Size);
|
||||
CornerRadius = 1.5f;
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
box = new Box
|
||||
{
|
||||
Colour = colourProvider.Light1,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Y = -2,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = "...",
|
||||
Colour = colourProvider.Background4,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
box.FadeColour(colourProvider.Content2, 300, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
box.FadeColour(colourProvider.Light1, 300, Easing.OutQuint);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
box.FlashColour(colourProvider.Content1, 300, Easing.OutQuint);
|
||||
this.ShowPopover();
|
||||
return true;
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new TagsOverflowPopover(tags, songSelect);
|
||||
}
|
||||
|
||||
public partial class TagsOverflowPopover : OsuPopover
|
||||
{
|
||||
private readonly string[] tags;
|
||||
private readonly SongSelect? songSelect;
|
||||
|
||||
public TagsOverflowPopover(string[] tags, SongSelect? songSelect)
|
||||
{
|
||||
this.tags = tags;
|
||||
this.songSelect = songSelect;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
LinkFlowContainer textFlow;
|
||||
|
||||
Child = textFlow = new LinkFlowContainer(t => t.Font = OsuFont.Style.Caption1)
|
||||
{
|
||||
Width = 200,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
};
|
||||
|
||||
foreach (string tag in tags)
|
||||
{
|
||||
textFlow.AddLink(tag, () => songSelect?.Search(tag));
|
||||
textFlow.AddText(" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// 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.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapMetadataWedge
|
||||
{
|
||||
private partial class UserRatingDisplay : CompositeDrawable
|
||||
{
|
||||
private readonly OsuSpriteText negativeText;
|
||||
private readonly OsuSpriteText positiveText;
|
||||
private readonly Circle backgroundBar;
|
||||
private readonly Circle positiveBar;
|
||||
|
||||
public int[] Data
|
||||
{
|
||||
set
|
||||
{
|
||||
const int rating_range = 10;
|
||||
|
||||
if (!value.Any())
|
||||
{
|
||||
negativeText.Text = 0.ToLocalisableString(@"N0");
|
||||
positiveText.Text = 0.ToLocalisableString(@"N0");
|
||||
positiveBar.ResizeWidthTo(0, 300, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
var usableRange = value.Skip(1).Take(rating_range); // adjust for API returning weird empty data at 0.
|
||||
|
||||
int positiveCount = usableRange.Skip(rating_range / 2).Sum();
|
||||
int totalCount = usableRange.Sum();
|
||||
|
||||
negativeText.Text = (totalCount - positiveCount).ToLocalisableString(@"N0");
|
||||
positiveText.Text = positiveCount.ToLocalisableString(@"N0");
|
||||
positiveBar.ResizeWidthTo(totalCount == 0 ? 0 : (float)positiveCount / totalCount, 300, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public UserRatingDisplay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = BeatmapsetsStrings.ShowStatsUserRating,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Top = 10f },
|
||||
Children = new[]
|
||||
{
|
||||
negativeText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Font = OsuFont.Style.Caption1,
|
||||
},
|
||||
positiveText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Font = OsuFont.Style.Caption1,
|
||||
},
|
||||
},
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new[]
|
||||
{
|
||||
backgroundBar = new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 4f,
|
||||
},
|
||||
positiveBar = new Circle
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0f,
|
||||
Height = 4f,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, OverlayColourProvider colourProvider)
|
||||
{
|
||||
backgroundBar.Colour = colours.DarkOrange2;
|
||||
positiveBar.Colour = colours.Lime1;
|
||||
negativeText.Colour = colourProvider.Content2;
|
||||
positiveText.Colour = colourProvider.Content2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
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;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
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 osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapTitleWedge : VisibilityContainer
|
||||
{
|
||||
private const float corner_radius = 10;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
protected override bool StartHidden => true;
|
||||
|
||||
private ModSettingChangeTracker? settingChangeTracker;
|
||||
|
||||
private BeatmapSetOnlineStatusPill statusPill = null!;
|
||||
private Container titleContainer = null!;
|
||||
private OsuHoverContainer titleLink = null!;
|
||||
private OsuSpriteText titleLabel = null!;
|
||||
private Container artistContainer = null!;
|
||||
private OsuHoverContainer artistLink = null!;
|
||||
private OsuSpriteText artistLabel = null!;
|
||||
|
||||
internal string DisplayedTitle => titleLabel.Text.ToString();
|
||||
internal string DisplayedArtist => artistLabel.Text.ToString();
|
||||
|
||||
private StatisticPlayCount playCount = null!;
|
||||
private Statistic favouritesStatistic = null!;
|
||||
private Statistic lengthStatistic = null!;
|
||||
private Statistic bpmStatistic = null!;
|
||||
|
||||
[Resolved]
|
||||
private SongSelect? songSelect { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private LocalisationManager localisation { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private APIBeatmapSet? currentOnlineBeatmapSet;
|
||||
private GetBeatmapSetRequest? currentRequest;
|
||||
|
||||
private FillFlowContainer statisticsFlow = null!;
|
||||
|
||||
public BeatmapTitleWedge()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Shear = OsuGame.SHEAR;
|
||||
Masking = true;
|
||||
CornerRadius = corner_radius;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new WedgeBackground(),
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = SongSelect.WEDGE_CONTENT_MARGIN,
|
||||
Left = SongSelect.WEDGE_CONTENT_MARGIN
|
||||
},
|
||||
Spacing = new Vector2(0f, 4f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ShearAligningWrapper(statusPill = new BeatmapSetOnlineStatusPill
|
||||
{
|
||||
Shear = -OsuGame.SHEAR,
|
||||
ShowUnknownStatus = true,
|
||||
TextSize = OsuFont.Style.Caption1.Size,
|
||||
TextPadding = new MarginPadding { Horizontal = 6, Vertical = 1 },
|
||||
}),
|
||||
new ShearAligningWrapper(titleContainer = new Container
|
||||
{
|
||||
Shear = -OsuGame.SHEAR,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = OsuFont.Style.Title.Size,
|
||||
Margin = new MarginPadding { Bottom = -4f },
|
||||
Child = titleLink = new OsuHoverContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = titleLabel = new TruncatingSpriteText
|
||||
{
|
||||
Shadow = true,
|
||||
Font = OsuFont.Style.Title,
|
||||
},
|
||||
}
|
||||
}),
|
||||
new ShearAligningWrapper(artistContainer = new Container
|
||||
{
|
||||
Shear = -OsuGame.SHEAR,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = OsuFont.Style.Heading2.Size,
|
||||
Margin = new MarginPadding { Left = 1f },
|
||||
Child = artistLink = new OsuHoverContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = artistLabel = new TruncatingSpriteText
|
||||
{
|
||||
Shadow = true,
|
||||
Font = OsuFont.Style.Heading2,
|
||||
},
|
||||
}
|
||||
}),
|
||||
new ShearAligningWrapper(statisticsFlow = new FillFlowContainer
|
||||
{
|
||||
Shear = -OsuGame.SHEAR,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(2f, 0f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
playCount = new StatisticPlayCount(background: true, leftPadding: SongSelect.WEDGE_CONTENT_MARGIN, minSize: 50f)
|
||||
{
|
||||
Margin = new MarginPadding { Left = -SongSelect.WEDGE_CONTENT_MARGIN },
|
||||
},
|
||||
favouritesStatistic = new Statistic(OsuIcon.Heart, background: true, minSize: 25f)
|
||||
{
|
||||
TooltipText = BeatmapsStrings.StatusFavourites,
|
||||
},
|
||||
lengthStatistic = new Statistic(OsuIcon.Clock),
|
||||
bpmStatistic = new Statistic(OsuIcon.Metronome)
|
||||
{
|
||||
TooltipText = BeatmapsetsStrings.ShowStatsBpm,
|
||||
Margin = new MarginPadding { Left = 5f },
|
||||
},
|
||||
},
|
||||
}),
|
||||
new ShearAligningWrapper(new Container
|
||||
{
|
||||
Shear = -OsuGame.SHEAR,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Left = -SongSelect.WEDGE_CONTENT_MARGIN },
|
||||
Padding = new MarginPadding { Right = -SongSelect.WEDGE_CONTENT_MARGIN },
|
||||
Child = new DifficultyDisplay(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
beatmap.BindValueChanged(_ => updateDisplay());
|
||||
ruleset.BindValueChanged(_ => updateDisplay());
|
||||
|
||||
mods.BindValueChanged(m =>
|
||||
{
|
||||
settingChangeTracker?.Dispose();
|
||||
|
||||
updateLengthAndBpmStatistics();
|
||||
|
||||
settingChangeTracker = new ModSettingChangeTracker(m.NewValue);
|
||||
settingChangeTracker.SettingChanged += _ => updateLengthAndBpmStatistics();
|
||||
});
|
||||
|
||||
updateDisplay();
|
||||
|
||||
statisticsFlow.AutoSizeDuration = 100;
|
||||
statisticsFlow.AutoSizeEasing = Easing.OutQuint;
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.MoveToX(0, SongSelect.ENTER_DURATION, Easing.OutQuint)
|
||||
.FadeIn(SongSelect.ENTER_DURATION / 3, Easing.In);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.MoveToX(-150, SongSelect.ENTER_DURATION, Easing.OutQuint)
|
||||
.FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
titleLabel.MaxWidth = titleContainer.DrawWidth - 20;
|
||||
artistLabel.MaxWidth = artistContainer.DrawWidth - 20;
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
var metadata = beatmap.Value.Metadata;
|
||||
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
|
||||
|
||||
statusPill.Status = beatmapInfo.Status;
|
||||
|
||||
var titleText = new RomanisableString(metadata.TitleUnicode, metadata.Title);
|
||||
titleLabel.Text = titleText;
|
||||
titleLink.Action = () => songSelect?.Search(titleText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript));
|
||||
|
||||
var artistText = new RomanisableString(metadata.ArtistUnicode, metadata.Artist);
|
||||
artistLabel.Text = artistText;
|
||||
artistLink.Action = () => songSelect?.Search(artistText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript));
|
||||
|
||||
updateLengthAndBpmStatistics();
|
||||
|
||||
if (currentOnlineBeatmapSet == null || currentOnlineBeatmapSet.OnlineID != beatmapSetInfo.OnlineID)
|
||||
refetchBeatmapSet();
|
||||
|
||||
updateOnlineDisplay();
|
||||
}
|
||||
|
||||
private void updateLengthAndBpmStatistics()
|
||||
{
|
||||
var beatmapInfo = beatmap.Value.BeatmapInfo;
|
||||
|
||||
double rate = ModUtils.CalculateRateWithMods(mods.Value);
|
||||
|
||||
int bpmMax = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMaximum, rate);
|
||||
int bpmMin = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMinimum, rate);
|
||||
int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.Value.Beatmap.GetMostCommonBeatLength(), rate);
|
||||
|
||||
double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate);
|
||||
double hitLength = Math.Round(beatmapInfo.Length / rate);
|
||||
|
||||
lengthStatistic.Text = hitLength.ToFormattedDuration();
|
||||
lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration());
|
||||
|
||||
bpmStatistic.Text = bpmMin == bpmMax
|
||||
? $"{bpmMin}"
|
||||
: $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})";
|
||||
}
|
||||
|
||||
private void refetchBeatmapSet()
|
||||
{
|
||||
var beatmapSetInfo = beatmap.Value.BeatmapSetInfo;
|
||||
|
||||
currentRequest?.Cancel();
|
||||
currentRequest = null;
|
||||
currentOnlineBeatmapSet = null;
|
||||
|
||||
if (beatmapSetInfo.OnlineID >= 1)
|
||||
{
|
||||
// todo: consider introducing a BeatmapSetLookupCache for caching benefits.
|
||||
currentRequest = new GetBeatmapSetRequest(beatmapSetInfo.OnlineID);
|
||||
currentRequest.Failure += _ => updateOnlineDisplay();
|
||||
currentRequest.Success += s =>
|
||||
{
|
||||
currentOnlineBeatmapSet = s;
|
||||
updateOnlineDisplay();
|
||||
};
|
||||
|
||||
api.Queue(currentRequest);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateOnlineDisplay()
|
||||
{
|
||||
if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting)
|
||||
{
|
||||
playCount.Value = null;
|
||||
favouritesStatistic.Text = null;
|
||||
}
|
||||
else if (currentOnlineBeatmapSet == null)
|
||||
{
|
||||
playCount.Value = new StatisticPlayCount.Data(-1, -1);
|
||||
favouritesStatistic.Text = "-";
|
||||
}
|
||||
else
|
||||
{
|
||||
var onlineBeatmapSet = currentOnlineBeatmapSet;
|
||||
var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmap.Value.BeatmapInfo.OnlineID);
|
||||
|
||||
if (onlineBeatmap != null)
|
||||
{
|
||||
playCount.FadeIn(300, Easing.OutQuint);
|
||||
playCount.Value = new StatisticPlayCount.Data(onlineBeatmap.PlayCount, onlineBeatmap.UserPlayCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
playCount.FadeOut(300, Easing.OutQuint);
|
||||
playCount.Value = null;
|
||||
}
|
||||
|
||||
favouritesStatistic.FadeIn(300, Easing.OutQuint);
|
||||
favouritesStatistic.Text = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
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.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapTitleWedge
|
||||
{
|
||||
public partial class DifficultyDisplay : CompositeDrawable
|
||||
{
|
||||
private const float border_weight = 2;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
private ModSettingChangeTracker? settingChangeTracker;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private StarRatingDisplay starRatingDisplay = null!;
|
||||
private FillFlowContainer nameLine = null!;
|
||||
private OsuSpriteText difficultyText = null!;
|
||||
private OsuSpriteText mappedByText = null!;
|
||||
private OsuHoverContainer mapperLink = null!;
|
||||
private OsuSpriteText mapperText = null!;
|
||||
|
||||
internal LocalisableString DisplayedVersion => difficultyText.Text;
|
||||
internal LocalisableString DisplayedAuthor => mapperText.Text;
|
||||
|
||||
private GridContainer ratingAndNameContainer = null!;
|
||||
private DifficultyStatisticsDisplay countStatisticsDisplay = null!;
|
||||
private AdjustableDifficultyStatisticsDisplay difficultyStatisticsDisplay = null!;
|
||||
|
||||
private CancellationTokenSource? cancellationSource;
|
||||
|
||||
public DifficultyDisplay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Masking = true;
|
||||
CornerRadius = 10;
|
||||
Shear = OsuGame.SHEAR;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new WedgeBackground(),
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ShearAligningWrapper(ratingAndNameContainer = new GridContainer
|
||||
{
|
||||
Shear = -OsuGame.SHEAR,
|
||||
AlwaysPresent = true,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Vertical = 5f },
|
||||
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN },
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Absolute, 6),
|
||||
new Dimension(),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
starRatingDisplay = new StarRatingDisplay(default, animated: true)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
Empty(),
|
||||
nameLine = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Margin = new MarginPadding { Bottom = 2f },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
difficultyText = new TruncatingSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
mappedByText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = " mapped by ",
|
||||
Font = OsuFont.Style.Body,
|
||||
},
|
||||
mapperLink = new MapperLinkContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Child = mapperText = new TruncatingSpriteText
|
||||
{
|
||||
Shadow = true,
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}),
|
||||
new ShearAligningWrapper(new Container
|
||||
{
|
||||
Shear = -OsuGame.SHEAR,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Bottom = border_weight, Right = border_weight },
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Masking = true,
|
||||
CornerRadius = 10 - border_weight,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5.Opacity(0.8f),
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 20f, Vertical = 7.5f },
|
||||
Shear = -OsuGame.SHEAR,
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 30),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
countStatisticsDisplay = new DifficultyStatisticsDisplay
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
Empty(),
|
||||
difficultyStatisticsDisplay = new AdjustableDifficultyStatisticsDisplay(autoSize: true),
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
beatmap.BindValueChanged(_ => updateDisplay());
|
||||
ruleset.BindValueChanged(_ => updateDisplay());
|
||||
|
||||
mods.BindValueChanged(m =>
|
||||
{
|
||||
settingChangeTracker?.Dispose();
|
||||
|
||||
updateDifficultyStatistics();
|
||||
|
||||
settingChangeTracker = new ModSettingChangeTracker(m.NewValue);
|
||||
settingChangeTracker.SettingChanged += _ => updateDifficultyStatistics();
|
||||
});
|
||||
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private ILinkHandler? linkHandler { get; set; }
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
cancellationSource?.Cancel();
|
||||
cancellationSource = new CancellationTokenSource();
|
||||
|
||||
computeStarDifficulty(cancellationSource.Token);
|
||||
|
||||
if (beatmap.IsDefault)
|
||||
{
|
||||
ratingAndNameContainer.FadeOut(300, Easing.OutQuint);
|
||||
countStatisticsDisplay.Statistics = Array.Empty<StatisticDifficulty.Data>();
|
||||
}
|
||||
else
|
||||
{
|
||||
ratingAndNameContainer.FadeIn(300, Easing.OutQuint);
|
||||
difficultyText.Text = beatmap.Value.BeatmapInfo.DifficultyName;
|
||||
mapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, beatmap.Value.Metadata.Author));
|
||||
mapperText.Text = beatmap.Value.Metadata.Author.Username;
|
||||
|
||||
var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value);
|
||||
|
||||
countStatisticsDisplay.Statistics = playableBeatmap.GetStatistics()
|
||||
.Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
updateDifficultyStatistics();
|
||||
}
|
||||
|
||||
private void updateDifficultyStatistics() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
if (beatmap.IsDefault)
|
||||
{
|
||||
difficultyStatisticsDisplay.TooltipContent = null;
|
||||
difficultyStatisticsDisplay.Statistics = Array.Empty<StatisticDifficulty.Data>();
|
||||
return;
|
||||
}
|
||||
|
||||
BeatmapDifficulty baseDifficulty = beatmap.Value.BeatmapInfo.Difficulty;
|
||||
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(baseDifficulty);
|
||||
|
||||
foreach (var mod in mods.Value.OfType<IApplicableToDifficulty>())
|
||||
mod.ApplyToDifficulty(originalDifficulty);
|
||||
|
||||
var rateAdjustedDifficulty = originalDifficulty;
|
||||
|
||||
if (ruleset.Value != null)
|
||||
{
|
||||
double rate = ModUtils.CalculateRateWithMods(mods.Value);
|
||||
|
||||
rateAdjustedDifficulty = ruleset.Value.CreateInstance().GetRateAdjustedDisplayDifficulty(originalDifficulty, rate);
|
||||
difficultyStatisticsDisplay.TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, rateAdjustedDifficulty);
|
||||
}
|
||||
|
||||
StatisticDifficulty.Data firstStatistic;
|
||||
|
||||
switch (ruleset.Value?.OnlineID)
|
||||
{
|
||||
case 3:
|
||||
// Account for mania differences locally for now.
|
||||
// Eventually this should be handled in a more modular way, allowing rulesets to return arbitrary difficulty attributes.
|
||||
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
|
||||
|
||||
// For the time being, the key count is static no matter what, because:
|
||||
// a) The method doesn't have knowledge of the active keymods. Doing so may require considerations for filtering.
|
||||
// b) Using the difficulty adjustment mod to adjust OD doesn't have an effect on conversion.
|
||||
int keyCount = legacyRuleset.GetKeyCount(beatmap.Value.BeatmapInfo, mods.Value);
|
||||
|
||||
firstStatistic = new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsCsMania, keyCount, keyCount, 10);
|
||||
break;
|
||||
|
||||
default:
|
||||
firstStatistic = new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsCs, baseDifficulty.CircleSize, rateAdjustedDifficulty.CircleSize, 10);
|
||||
break;
|
||||
}
|
||||
|
||||
difficultyStatisticsDisplay.Statistics = new[]
|
||||
{
|
||||
firstStatistic,
|
||||
new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsAccuracy, baseDifficulty.OverallDifficulty, rateAdjustedDifficulty.OverallDifficulty, 10),
|
||||
new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsDrain, baseDifficulty.DrainRate, rateAdjustedDifficulty.DrainRate, 10),
|
||||
new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsAr, baseDifficulty.ApproachRate, rateAdjustedDifficulty.ApproachRate, 10),
|
||||
};
|
||||
});
|
||||
|
||||
private void computeStarDifficulty(CancellationToken cancellationToken)
|
||||
{
|
||||
difficultyCache.GetDifficultyAsync(beatmap.Value.BeatmapInfo, ruleset.Value, mods.Value, cancellationToken)
|
||||
.ContinueWith(task =>
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
starRatingDisplay.Current.Value = task.GetResultSafely() ?? default;
|
||||
});
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
difficultyText.MaxWidth = Math.Max(nameLine.DrawWidth - mappedByText.DrawWidth - mapperText.DrawWidth - 20, 0);
|
||||
|
||||
// Use difficulty colour until it gets too dark to be visible against dark backgrounds.
|
||||
Color4 col = starRatingDisplay.DisplayedStars.Value >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : starRatingDisplay.DisplayedDifficultyColour;
|
||||
|
||||
difficultyText.Colour = col;
|
||||
mappedByText.Colour = col;
|
||||
countStatisticsDisplay.AccentColour = col;
|
||||
difficultyStatisticsDisplay.AccentColour = col;
|
||||
}
|
||||
|
||||
private partial class MapperLinkContainer : OsuHoverContainer
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours)
|
||||
{
|
||||
TooltipText = ContextMenuStrings.ViewProfile;
|
||||
IdleColour = overlayColourProvider?.Light2 ?? colours.Blue;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class AdjustableDifficultyStatisticsDisplay : DifficultyStatisticsDisplay, IHasCustomTooltip<AdjustedAttributesTooltip.Data>
|
||||
{
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public ITooltip<AdjustedAttributesTooltip.Data> GetCustomTooltip() => new AdjustedAttributesTooltip(colourProvider);
|
||||
|
||||
public AdjustedAttributesTooltip.Data? TooltipContent { get; set; }
|
||||
|
||||
public AdjustableDifficultyStatisticsDisplay(bool autoSize)
|
||||
: base(autoSize)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
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.Layout;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapTitleWedge
|
||||
{
|
||||
public partial class DifficultyStatisticsDisplay : CompositeDrawable
|
||||
{
|
||||
private readonly bool autoSize;
|
||||
private readonly FillFlowContainer<StatisticDifficulty> statisticsFlow;
|
||||
private readonly GridContainer tinyStatisticsGrid;
|
||||
|
||||
private IReadOnlyList<StatisticDifficulty.Data> statistics = Array.Empty<StatisticDifficulty.Data>();
|
||||
|
||||
public IReadOnlyList<StatisticDifficulty.Data> Statistics
|
||||
{
|
||||
get => statistics;
|
||||
set
|
||||
{
|
||||
statistics = value;
|
||||
|
||||
if (IsLoaded)
|
||||
{
|
||||
updateStatistics();
|
||||
updateTinyStatistics();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Color4 accentColour;
|
||||
|
||||
public Color4 AccentColour
|
||||
{
|
||||
get => accentColour;
|
||||
set
|
||||
{
|
||||
if (accentColour == value)
|
||||
return;
|
||||
|
||||
accentColour = value;
|
||||
|
||||
foreach (var statistic in statisticsFlow)
|
||||
statistic.AccentColour = value;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize);
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public DifficultyStatisticsDisplay(bool autoSize = false)
|
||||
{
|
||||
this.autoSize = autoSize;
|
||||
|
||||
if (autoSize)
|
||||
AutoSizeAxes = Axes.Both;
|
||||
else
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
statisticsFlow = new FillFlowContainer<StatisticDifficulty>
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(8f, 0f),
|
||||
Direction = FillDirection.Horizontal,
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
tinyStatisticsGrid = new GridContainer
|
||||
{
|
||||
Alpha = 0f,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Absolute, 8),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
AddLayout(drawSizeLayout);
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private LocalisationManager localisations { get; set; } = null!;
|
||||
|
||||
private IBindable<LocalisationParameters>? localisationParameters;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
localisationParameters = localisations.CurrentParameters.GetBoundCopy();
|
||||
localisationParameters.BindValueChanged(_ => updateStatisticsSizing());
|
||||
|
||||
updateStatistics();
|
||||
updateTinyStatistics();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!drawSizeLayout.IsValid)
|
||||
{
|
||||
updateLayout();
|
||||
drawSizeLayout.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
private bool displayedTinyStatistics;
|
||||
|
||||
private void updateLayout()
|
||||
{
|
||||
if (statisticsFlow.Count == 0)
|
||||
return;
|
||||
|
||||
float flowWidth = statisticsFlow[0].Width * statisticsFlow.Count + statisticsFlow.Spacing.X * (statisticsFlow.Count - 1);
|
||||
bool tiny = !autoSize && DrawWidth < flowWidth;
|
||||
|
||||
if (displayedTinyStatistics != tiny)
|
||||
{
|
||||
if (tiny)
|
||||
{
|
||||
statisticsFlow.Hide();
|
||||
tinyStatisticsGrid.FadeIn(200, Easing.InQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
tinyStatisticsGrid.Hide();
|
||||
statisticsFlow.FadeIn(200, Easing.InQuint);
|
||||
}
|
||||
|
||||
displayedTinyStatistics = tiny;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStatisticsSizing() => SchedulerAfterChildren.AddOnce(() =>
|
||||
{
|
||||
if (statisticsFlow.Count == 0)
|
||||
return;
|
||||
|
||||
float statisticWidth = Math.Max(65, statisticsFlow.Max(s => s.LabelWidth));
|
||||
|
||||
foreach (var statistic in statisticsFlow)
|
||||
statistic.Width = statisticWidth;
|
||||
|
||||
drawSizeLayout.Invalidate();
|
||||
});
|
||||
|
||||
private void updateStatistics()
|
||||
{
|
||||
if (statisticsFlow.Select(s => s.Value.Label)
|
||||
.SequenceEqual(statistics.Select(s => s.Label)))
|
||||
{
|
||||
for (int i = 0; i < statistics.Count; i++)
|
||||
statisticsFlow[i].Value = statistics[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
statisticsFlow.ChildrenEnumerable = statistics.Select(d => new StatisticDifficulty { Value = d });
|
||||
updateStatisticsSizing();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTinyStatistics()
|
||||
{
|
||||
tinyStatisticsGrid.RowDimensions = statistics.Select(_ => new Dimension(GridSizeMode.AutoSize)).ToArray();
|
||||
tinyStatisticsGrid.Content = statistics.Select(s => new[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = s.Label,
|
||||
Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold),
|
||||
Colour = colourProvider.Content2,
|
||||
},
|
||||
Empty(),
|
||||
new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold),
|
||||
Text = s.Content ?? s.Value.ToLocalisableString("0.##"),
|
||||
Colour = colourProvider.Content1,
|
||||
},
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapTitleWedge
|
||||
{
|
||||
public partial class Statistic : CompositeDrawable, IHasTooltip
|
||||
{
|
||||
private readonly IconUsage icon;
|
||||
private readonly bool background;
|
||||
private readonly float leftPadding;
|
||||
private readonly float? minSize;
|
||||
|
||||
private OsuSpriteText valueText = null!;
|
||||
private LoadingSpinner loading = null!;
|
||||
|
||||
private LocalisableString? text;
|
||||
|
||||
public LocalisableString? Text
|
||||
{
|
||||
get => text;
|
||||
set
|
||||
{
|
||||
text = value;
|
||||
Scheduler.AddOnce(updateDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
public LocalisableString TooltipText { get; set; }
|
||||
|
||||
public Statistic(IconUsage icon, bool background = false, float leftPadding = 10f, float? minSize = null)
|
||||
{
|
||||
this.icon = icon;
|
||||
this.background = background;
|
||||
this.leftPadding = leftPadding;
|
||||
this.minSize = minSize;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
Shear = background ? OsuGame.SHEAR : Vector2.Zero;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
Alpha = background ? 0.2f : 0f,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Margin = new MarginPadding { Left = background ? leftPadding : 0, Right = background ? 10f : 0f, Vertical = 5f },
|
||||
Spacing = new Vector2(4f, 0f),
|
||||
Shear = background ? -OsuGame.SHEAR : Vector2.Zero,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Icon = icon,
|
||||
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[]
|
||||
{
|
||||
loading = new LoadingSpinner
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(14f),
|
||||
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: minSize ?? 0),
|
||||
},
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Scheduler.AddOnce(updateDisplay);
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
loading.State.Value = text != null ? Visibility.Hidden : Visibility.Visible;
|
||||
|
||||
if (text != null)
|
||||
{
|
||||
valueText.Text = text.Value;
|
||||
valueText.FadeIn(120, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
valueText.FadeOut(120, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
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 osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapTitleWedge
|
||||
{
|
||||
public partial class StatisticDifficulty : CompositeDrawable, IHasAccentColour
|
||||
{
|
||||
private Data value = new Data(string.Empty, 0, 0, 0);
|
||||
|
||||
public Data Value
|
||||
{
|
||||
get => value;
|
||||
set
|
||||
{
|
||||
this.value = value;
|
||||
|
||||
if (IsLoaded)
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
public float LabelWidth => labelText.DrawWidth;
|
||||
|
||||
private readonly Circle bar;
|
||||
private readonly Circle adjustedBar;
|
||||
private readonly OsuSpriteText labelText;
|
||||
private readonly OsuSpriteText valueText;
|
||||
private readonly SpriteIcon valueIcon;
|
||||
private readonly Container bars;
|
||||
|
||||
public Color4 AccentColour
|
||||
{
|
||||
get => bar.Colour;
|
||||
set => bar.Colour = value;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
public StatisticDifficulty()
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
bars = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new[]
|
||||
{
|
||||
new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 2f,
|
||||
Colour = Color4.Black,
|
||||
Masking = true,
|
||||
CornerRadius = 1f,
|
||||
Depth = float.MaxValue,
|
||||
},
|
||||
bar = new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0f,
|
||||
Height = 2f,
|
||||
Masking = true,
|
||||
CornerRadius = 1f,
|
||||
},
|
||||
adjustedBar = new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0f,
|
||||
Height = 2f,
|
||||
Masking = true,
|
||||
CornerRadius = 1f,
|
||||
},
|
||||
},
|
||||
},
|
||||
labelText = new OsuSpriteText
|
||||
{
|
||||
Margin = new MarginPadding { Top = 2f },
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
valueText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.Style.Body,
|
||||
},
|
||||
valueIcon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Top = -4f,
|
||||
Left = 2,
|
||||
},
|
||||
Size = new Vector2(8),
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
labelText.Colour = colourProvider.Content2;
|
||||
valueText.Colour = colourProvider.Content1;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
bar.ResizeWidthTo(value.Maximum == 0 ? 0 : Math.Clamp(value.Value / value.Maximum, 0, 1), 300, Easing.OutQuint);
|
||||
adjustedBar.ResizeWidthTo(value.Maximum == 0 ? 0 : Math.Clamp(value.AdjustedValue / value.Maximum, 0, 1), 300, Easing.OutQuint);
|
||||
|
||||
labelText.Text = value.Label;
|
||||
valueText.Text = value.Content ?? value.AdjustedValue.ToLocalisableString("0.##");
|
||||
|
||||
if (value.Value == value.AdjustedValue)
|
||||
{
|
||||
adjustedBar.FadeColour(Color4.Transparent, 300, Easing.OutQuint);
|
||||
bar.FadeIn(300, Easing.OutQuint);
|
||||
|
||||
valueText.FadeColour(Color4.White, 300, Easing.OutQuint);
|
||||
valueIcon.Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
bool difficultyIncrease = value.Value < value.AdjustedValue;
|
||||
|
||||
if (difficultyIncrease)
|
||||
{
|
||||
bars.ChangeChildDepth(adjustedBar, 1);
|
||||
bar.FadeIn(300, Easing.OutQuint);
|
||||
adjustedBar.FadeColour(ColourInfo.GradientHorizontal(Color4.Black, colours.Red1), 300, Easing.OutQuint);
|
||||
|
||||
valueText.FadeColour(colours.Red1, 300, Easing.OutQuint);
|
||||
valueIcon.Show();
|
||||
valueIcon.Colour = colours.Red1;
|
||||
valueIcon.Icon = FontAwesome.Solid.SortUp;
|
||||
}
|
||||
else
|
||||
{
|
||||
bar.FadeTo(0.5f, 300, Easing.OutQuint);
|
||||
bars.ChangeChildDepth(adjustedBar, -1);
|
||||
adjustedBar.FadeColour(colours.Lime1, 300, Easing.OutQuint);
|
||||
|
||||
valueText.FadeColour(colours.Lime1, 300, Easing.OutQuint);
|
||||
valueIcon.Show();
|
||||
valueIcon.Colour = colours.Lime1;
|
||||
valueIcon.Icon = FontAwesome.Solid.SortDown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record Data(LocalisableString Label, float Value, float AdjustedValue, float Maximum, string? Content = null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapTitleWedge
|
||||
{
|
||||
public partial class StatisticPlayCount : Statistic, IHasCustomTooltip<StatisticPlayCount.Data>
|
||||
{
|
||||
public Data? Value
|
||||
{
|
||||
set
|
||||
{
|
||||
base.Text = value?.Total < 0 ? "-" : value?.Total.ToLocalisableString("N0");
|
||||
TooltipContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
public new LocalisableString? Text
|
||||
{
|
||||
set => throw new InvalidOperationException($"Use {nameof(Value)} instead.");
|
||||
}
|
||||
|
||||
public Data? TooltipContent { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public StatisticPlayCount(bool background = false, float leftPadding = 10, float? minSize = null)
|
||||
: base(OsuIcon.Play, background, leftPadding, minSize)
|
||||
{
|
||||
}
|
||||
|
||||
ITooltip<Data> IHasCustomTooltip<Data>.GetCustomTooltip() => new PlayCountTooltip(colourProvider);
|
||||
|
||||
public record Data(int Total, int User);
|
||||
|
||||
private partial class PlayCountTooltip : VisibilityContainer, ITooltip<Data>
|
||||
{
|
||||
private readonly OverlayColourProvider colourProvider;
|
||||
|
||||
private OsuSpriteText totalPlaysText = null!;
|
||||
private OsuSpriteText personalPlaysText = null!;
|
||||
|
||||
public PlayCountTooltip(OverlayColourProvider colourProvider)
|
||||
{
|
||||
this.colourProvider = colourProvider;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
CornerRadius = 10;
|
||||
Masking = true;
|
||||
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Radius = 10f,
|
||||
};
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding(10),
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(16f, 0f),
|
||||
Children = new[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
Text = "Total Plays",
|
||||
},
|
||||
totalPlaysText = new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content1,
|
||||
Font = OsuFont.Style.Heading1.With(weight: FontWeight.Regular),
|
||||
},
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
Text = "Personal Plays",
|
||||
},
|
||||
personalPlaysText = new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content1,
|
||||
Font = OsuFont.Style.Heading1.With(weight: FontWeight.Regular),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public void SetContent(Data content)
|
||||
{
|
||||
totalPlaysText.Text = content.Total < 0 ? "-" : content.Total.ToLocalisableString("N0");
|
||||
personalPlaysText.Text = content.User < 0 ? "-" : content.User.ToLocalisableString("N0");
|
||||
}
|
||||
|
||||
public void Move(Vector2 pos) => Position = pos;
|
||||
|
||||
protected override void PopIn() => this.FadeIn(300, Easing.OutQuint);
|
||||
protected override void PopOut() => this.FadeOut(300, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,7 +236,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
starRatingDisplay.Current.Value = starDifficulty;
|
||||
starCounter.Current = (float)starDifficulty.Stars;
|
||||
|
||||
difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
|
||||
difficultyIcon.FadeColour(starDifficulty.Stars > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
|
||||
|
||||
var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars);
|
||||
starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint);
|
||||
|
||||
@@ -270,7 +270,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
var starDifficulty = starDifficultyBindable?.Value ?? default;
|
||||
|
||||
AccentColour = colours.ForStarDifficulty(starDifficulty.Stars);
|
||||
difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
|
||||
difficultyIcon.FadeColour(starDifficulty.Stars > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
|
||||
difficultyStarRating.Current.Value = starDifficulty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ using osu.Game.Overlays;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@@ -22,8 +23,10 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Users.Drawables;
|
||||
@@ -80,6 +83,9 @@ namespace osu.Game.Users
|
||||
[Resolved]
|
||||
private MetadataClient? metadataClient { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private INotificationOverlay? notifications { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@@ -157,6 +163,10 @@ namespace osu.Game.Users
|
||||
chatOverlay?.Show();
|
||||
}));
|
||||
|
||||
items.Add(isUserBlocked()
|
||||
? new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => toggleBlock(false))
|
||||
: new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => toggleBlock(true)));
|
||||
|
||||
if (isUserOnline())
|
||||
{
|
||||
items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () =>
|
||||
@@ -179,9 +189,31 @@ namespace osu.Game.Users
|
||||
|
||||
bool isUserOnline() => metadataClient?.GetPresence(User.OnlineID) != null;
|
||||
bool canInviteUser() => isUserOnline() && multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true;
|
||||
bool isUserBlocked() => api.Blocks.Any(b => b.TargetID == User.OnlineID);
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleBlock(bool block)
|
||||
{
|
||||
APIRequest req = block ? new BlockUserRequest(User.OnlineID) : new UnblockUserRequest(User.OnlineID);
|
||||
|
||||
req.Success += () =>
|
||||
{
|
||||
api.UpdateLocalBlocks();
|
||||
};
|
||||
|
||||
req.Failure += e =>
|
||||
{
|
||||
notifications?.Post(new SimpleNotification
|
||||
{
|
||||
Text = e.Message,
|
||||
Icon = FontAwesome.Solid.Times,
|
||||
});
|
||||
};
|
||||
|
||||
api.Queue(req);
|
||||
}
|
||||
|
||||
public IEnumerable<LocalisableString> FilterTerms => [User.Username];
|
||||
|
||||
public bool MatchingFilter
|
||||
|
||||
Reference in New Issue
Block a user