mirror of
https://github.com/ppy/osu.git
synced 2025-03-11 06:37:19 +08:00
Merge branch 'master' into carousel-v2-implement-designs
This commit is contained in:
commit
d8f3dbf988
@ -173,7 +173,7 @@ namespace osu.Desktop
|
||||
new Button
|
||||
{
|
||||
Label = "View beatmap",
|
||||
Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
|
||||
Url = $@"{api.Endpoints.WebsiteUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
52
osu.Game.Tests/Online/TestSceneMetadataClient.cs
Normal file
52
osu.Game.Tests/Online/TestSceneMetadataClient.cs
Normal file
@ -0,0 +1,52 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Game.Tests.Visual.Metadata;
|
||||
|
||||
namespace osu.Game.Tests.Online
|
||||
{
|
||||
[TestFixture]
|
||||
[HeadlessTest]
|
||||
public partial class TestSceneMetadataClient : OsuTestScene
|
||||
{
|
||||
private TestMetadataClient client = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
Child = client = new TestMetadataClient();
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestWatchingMultipleTimesInvokesServerMethodsOnce()
|
||||
{
|
||||
int countBegin = 0;
|
||||
int countEnd = 0;
|
||||
|
||||
IDisposable token1 = null!;
|
||||
IDisposable token2 = null!;
|
||||
|
||||
AddStep("setup", () =>
|
||||
{
|
||||
client.OnBeginWatchingUserPresence += () => countBegin++;
|
||||
client.OnEndWatchingUserPresence += () => countEnd++;
|
||||
});
|
||||
|
||||
AddStep("begin watching presence (1)", () => token1 = client.BeginWatchingUserPresence());
|
||||
AddAssert("server method invoked once", () => countBegin, () => Is.EqualTo(1));
|
||||
|
||||
AddStep("begin watching presence (2)", () => token2 = client.BeginWatchingUserPresence());
|
||||
AddAssert("server method not invoked a second time", () => countBegin, () => Is.EqualTo(1));
|
||||
|
||||
AddStep("end watching presence (1)", () => token1.Dispose());
|
||||
AddAssert("server method not invoked", () => countEnd, () => Is.EqualTo(0));
|
||||
|
||||
AddStep("end watching presence (2)", () => token2.Dispose());
|
||||
AddAssert("server method invoked once", () => countEnd, () => Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
}
|
@ -74,7 +74,6 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Solo]
|
||||
public void TestCommitPlacementViaRightClick()
|
||||
{
|
||||
Playfield playfield = null!;
|
||||
|
@ -0,0 +1,47 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit.Submission;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public partial class TestSceneSubmissionStageProgress : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[Test]
|
||||
public void TestAppearance()
|
||||
{
|
||||
SubmissionStageProgress progress = null!;
|
||||
|
||||
AddStep("create content", () => Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.8f),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Child = progress = new SubmissionStageProgress
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
StageDescription = "Frobnicating the foobarator...",
|
||||
}
|
||||
});
|
||||
AddStep("not started", () => progress.SetNotStarted());
|
||||
AddStep("indeterminate progress", () => progress.SetInProgress());
|
||||
AddStep("30% progress", () => progress.SetInProgress(0.3f));
|
||||
AddStep("70% progress", () => progress.SetInProgress(0.7f));
|
||||
AddStep("completed", () => progress.SetCompleted());
|
||||
AddStep("failed", () => progress.SetFailed("the foobarator has defrobnicated"));
|
||||
AddStep("failed with long message", () => progress.SetFailed("this is a very very very very VERY VEEEEEEEEEEEEEEEEEEEEEEEEERY long error message like you would never believe"));
|
||||
AddStep("canceled", () => progress.SetCanceled());
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Solo;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -234,6 +235,31 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoSubmissionWhenScoreZero()
|
||||
{
|
||||
prepareTestAPI(true);
|
||||
|
||||
createPlayerTest();
|
||||
|
||||
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
|
||||
|
||||
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||
AddUntilStep("wait for first result", () => Player.Results.Count > 0);
|
||||
|
||||
AddStep("add fake non-scoring hit", () =>
|
||||
{
|
||||
Player.ScoreProcessor.RevertResult(Player.Results.First());
|
||||
Player.ScoreProcessor.ApplyResult(new OsuJudgementResult(Beatmap.Value.Beatmap.HitObjects.First(), new IgnoreJudgement())
|
||||
{
|
||||
Type = HitResult.IgnoreHit,
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("exit", () => Player.Exit());
|
||||
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSubmissionOnExit()
|
||||
{
|
||||
|
@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
new APIMenuImage
|
||||
{
|
||||
Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png",
|
||||
Url = $@"{API.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023",
|
||||
Url = $@"{API.Endpoints.WebsiteUrl}/home/news/2023-12-21-project-loved-december-2023",
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -165,7 +165,6 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Solo]
|
||||
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
|
||||
{
|
||||
prepareBeatmap();
|
||||
|
@ -65,7 +65,9 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[Test]
|
||||
public void TestBasicDisplay()
|
||||
{
|
||||
AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence());
|
||||
IDisposable token = null!;
|
||||
|
||||
AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence());
|
||||
AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
|
||||
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<UserGridPanel>().FirstOrDefault()?.User.Id == 2);
|
||||
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.False);
|
||||
@ -78,14 +80,16 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null));
|
||||
AddUntilStep("Panel no longer present", () => !currentlyOnline.ChildrenOfType<UserGridPanel>().Any());
|
||||
AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence());
|
||||
AddStep("End watching user presence", () => token.Dispose());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserWasPlayingBeforeWatchingUserPresence()
|
||||
{
|
||||
IDisposable token = null!;
|
||||
|
||||
AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0));
|
||||
AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence());
|
||||
AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence());
|
||||
AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
|
||||
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<UserGridPanel>().FirstOrDefault()?.User.Id == 2);
|
||||
AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.True);
|
||||
@ -93,7 +97,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id));
|
||||
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.False);
|
||||
AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null));
|
||||
AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence());
|
||||
AddStep("End watching user presence", () => token.Dispose());
|
||||
}
|
||||
|
||||
internal partial class TestUserLookupCache : UserLookupCache
|
||||
|
@ -4,17 +4,18 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual.Metadata;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
@ -23,32 +24,32 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[TestFixture]
|
||||
public partial class TestSceneUserPanel : OsuTestScene
|
||||
{
|
||||
private readonly Bindable<UserActivity?> activity = new Bindable<UserActivity?>();
|
||||
private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>();
|
||||
|
||||
private UserGridPanel boundPanel1 = null!;
|
||||
private TestUserListPanel boundPanel2 = null!;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
[Cached(typeof(LocalUserStatisticsProvider))]
|
||||
private readonly TestUserStatisticsProvider statisticsProvider = new TestUserStatisticsProvider();
|
||||
|
||||
[Resolved]
|
||||
private IRulesetStore rulesetStore { get; set; } = null!;
|
||||
|
||||
private TestUserStatisticsProvider statisticsProvider = null!;
|
||||
private TestMetadataClient metadataClient = null!;
|
||||
private TestUserListPanel panel = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
activity.Value = null;
|
||||
status.Value = null;
|
||||
|
||||
Remove(statisticsProvider, false);
|
||||
Clear();
|
||||
Add(statisticsProvider);
|
||||
|
||||
Add(new FillFlowContainer
|
||||
Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(LocalUserStatisticsProvider), statisticsProvider = new TestUserStatisticsProvider()),
|
||||
(typeof(MetadataClient), metadataClient = new TestMetadataClient())
|
||||
],
|
||||
Children = new Drawable[]
|
||||
{
|
||||
statisticsProvider,
|
||||
metadataClient,
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -78,7 +79,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
|
||||
IsOnline = true
|
||||
}) { Width = 300 },
|
||||
boundPanel1 = new UserGridPanel(new APIUser
|
||||
new UserGridPanel(new APIUser
|
||||
{
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
@ -87,13 +88,12 @@ namespace osu.Game.Tests.Visual.Online
|
||||
IsSupporter = true,
|
||||
SupportLevel = 3,
|
||||
}) { Width = 300 },
|
||||
boundPanel2 = new TestUserListPanel(new APIUser
|
||||
panel = new TestUserListPanel(new APIUser
|
||||
{
|
||||
Username = @"Evast",
|
||||
Id = 8195163,
|
||||
CountryCode = CountryCode.BY,
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
CountryCode = CountryCode.AU,
|
||||
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
IsOnline = false,
|
||||
LastVisit = DateTimeOffset.Now
|
||||
}),
|
||||
new UserRankPanel(new APIUser
|
||||
@ -112,55 +112,54 @@ namespace osu.Game.Tests.Visual.Online
|
||||
CountryCode = CountryCode.AU,
|
||||
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
|
||||
}) { Width = 300 }
|
||||
}) { Width = 300 },
|
||||
new UserGridPanel(API.LocalUser.Value)
|
||||
{
|
||||
Width = 300
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
boundPanel1.Status.BindTo(status);
|
||||
boundPanel1.Activity.BindTo(activity);
|
||||
|
||||
boundPanel2.Status.BindTo(status);
|
||||
boundPanel2.Activity.BindTo(activity);
|
||||
metadataClient.BeginWatchingUserPresence();
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestUserStatus()
|
||||
{
|
||||
AddStep("online", () => status.Value = UserStatus.Online);
|
||||
AddStep("do not disturb", () => status.Value = UserStatus.DoNotDisturb);
|
||||
AddStep("offline", () => status.Value = UserStatus.Offline);
|
||||
AddStep("null status", () => status.Value = null);
|
||||
AddStep("online", () => setPresence(UserStatus.Online, null));
|
||||
AddStep("do not disturb", () => setPresence(UserStatus.DoNotDisturb, null));
|
||||
AddStep("offline", () => setPresence(UserStatus.Offline, null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserActivity()
|
||||
{
|
||||
AddStep("set online status", () => status.Value = UserStatus.Online);
|
||||
|
||||
AddStep("idle", () => activity.Value = null);
|
||||
AddStep("watching replay", () => activity.Value = new UserActivity.WatchingReplay(createScore(@"nats")));
|
||||
AddStep("spectating user", () => activity.Value = new UserActivity.SpectatingUser(createScore(@"mrekk")));
|
||||
AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0));
|
||||
AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1));
|
||||
AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2));
|
||||
AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3));
|
||||
AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap());
|
||||
AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo()));
|
||||
AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(new BeatmapInfo()));
|
||||
AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo()));
|
||||
AddStep("idle", () => setPresence(UserStatus.Online, null));
|
||||
AddStep("watching replay", () => setPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats"))));
|
||||
AddStep("spectating user", () => setPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk"))));
|
||||
AddStep("solo (osu!)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(0)));
|
||||
AddStep("solo (osu!taiko)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(1)));
|
||||
AddStep("solo (osu!catch)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(2)));
|
||||
AddStep("solo (osu!mania)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(3)));
|
||||
AddStep("choosing", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap()));
|
||||
AddStep("editing beatmap", () => setPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo())));
|
||||
AddStep("modding beatmap", () => setPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo())));
|
||||
AddStep("testing beatmap", () => setPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo())));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserActivityChange()
|
||||
{
|
||||
AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent);
|
||||
AddStep("set online status", () => status.Value = UserStatus.Online);
|
||||
AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent);
|
||||
AddStep("set choosing activity", () => activity.Value = new UserActivity.ChoosingBeatmap());
|
||||
AddStep("set offline status", () => status.Value = UserStatus.Offline);
|
||||
AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent);
|
||||
AddStep("set online status", () => status.Value = UserStatus.Online);
|
||||
AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent);
|
||||
AddAssert("visit message is visible", () => panel.LastVisitMessage.IsPresent);
|
||||
AddStep("set online status", () => setPresence(UserStatus.Online, null));
|
||||
AddAssert("visit message is not visible", () => !panel.LastVisitMessage.IsPresent);
|
||||
AddStep("set choosing activity", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap()));
|
||||
AddStep("set offline status", () => setPresence(UserStatus.Offline, null));
|
||||
AddAssert("visit message is visible", () => panel.LastVisitMessage.IsPresent);
|
||||
AddStep("set online status", () => setPresence(UserStatus.Online, null));
|
||||
AddAssert("visit message is not visible", () => !panel.LastVisitMessage.IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -185,6 +184,31 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalUserActivity()
|
||||
{
|
||||
AddStep("idle", () => setPresence(UserStatus.Online, null, API.LocalUser.Value.OnlineID));
|
||||
AddStep("watching replay", () => setPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")), API.LocalUser.Value.OnlineID));
|
||||
AddStep("spectating user", () => setPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")), API.LocalUser.Value.OnlineID));
|
||||
AddStep("solo (osu!)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(0), API.LocalUser.Value.OnlineID));
|
||||
AddStep("solo (osu!taiko)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(1), API.LocalUser.Value.OnlineID));
|
||||
AddStep("solo (osu!catch)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(2), API.LocalUser.Value.OnlineID));
|
||||
AddStep("solo (osu!mania)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(3), API.LocalUser.Value.OnlineID));
|
||||
AddStep("choosing", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap(), API.LocalUser.Value.OnlineID));
|
||||
AddStep("editing beatmap", () => setPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID));
|
||||
AddStep("modding beatmap", () => setPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID));
|
||||
AddStep("testing beatmap", () => setPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID));
|
||||
AddStep("set offline status", () => setPresence(UserStatus.Offline, null, API.LocalUser.Value.OnlineID));
|
||||
}
|
||||
|
||||
private void setPresence(UserStatus status, UserActivity? activity, int? userId = null)
|
||||
{
|
||||
if (status == UserStatus.Offline)
|
||||
metadataClient.UserPresenceUpdated(userId ?? panel.User.OnlineID, null);
|
||||
else
|
||||
metadataClient.UserPresenceUpdated(userId ?? panel.User.OnlineID, new UserPresence { Status = status, Activity = activity });
|
||||
}
|
||||
|
||||
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!);
|
||||
|
||||
private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo)
|
||||
|
@ -67,19 +67,19 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[Test]
|
||||
public void TestLink()
|
||||
{
|
||||
AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/");
|
||||
AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/");
|
||||
|
||||
AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_page");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Main_page");
|
||||
|
||||
AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/FAQ");
|
||||
|
||||
AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/Writing");
|
||||
|
||||
AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/Formatting");
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -62,12 +62,6 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
if (beatmapInfo != null)
|
||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
|
||||
});
|
||||
|
||||
AddToggleStep("toggle legacy classic skin", v =>
|
||||
{
|
||||
if (skins != null)
|
||||
skins.CurrentSkinInfo.Value = v ? skins.DefaultClassicSkin.SkinInfo : skins.CurrentSkinInfo.Default;
|
||||
});
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
@ -84,6 +78,16 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLegacySkin()
|
||||
{
|
||||
AddToggleStep("toggle legacy classic skin", v =>
|
||||
{
|
||||
if (skins != null)
|
||||
skins.CurrentSkinInfo.Value = v ? skins.DefaultClassicSkin.SkinInfo : skins.CurrentSkinInfo.Default;
|
||||
});
|
||||
}
|
||||
|
||||
private int onlineScoreID = 1;
|
||||
|
||||
[TestCase(1, ScoreRank.X, 0)]
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -17,10 +19,10 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
|
||||
@ -57,16 +59,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Scheduler.AddDelayed(updateStats, 100, true);
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public virtual void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
|
||||
CreateCarousel();
|
||||
|
||||
SortBy(new FilterCriteria { Sort = SortMode.Title });
|
||||
}
|
||||
|
||||
protected void CreateCarousel()
|
||||
{
|
||||
AddStep("create components", () =>
|
||||
@ -134,7 +126,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
}
|
||||
|
||||
protected void SortBy(FilterCriteria criteria) => AddStep($"sort {criteria.Sort} group {criteria.Group}", () => Carousel.Filter(criteria));
|
||||
protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria));
|
||||
|
||||
protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType<ICarouselPanel>().Count(), () => Is.GreaterThan(0));
|
||||
protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False);
|
||||
@ -150,6 +142,9 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
|
||||
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
|
||||
|
||||
protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.Selected.Value);
|
||||
protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
|
||||
|
||||
protected void WaitForGroupSelection(int group, int panel)
|
||||
{
|
||||
AddUntilStep($"selected is group{group} panel{panel}", () =>
|
||||
@ -175,6 +170,15 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
}
|
||||
|
||||
protected IEnumerable<T> GetVisiblePanels<T>()
|
||||
where T : Drawable
|
||||
{
|
||||
return Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
|
||||
.ChildrenOfType<T>()
|
||||
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
|
||||
.OrderBy(p => p.Y);
|
||||
}
|
||||
|
||||
protected void ClickVisiblePanel<T>(int index)
|
||||
where T : Drawable
|
||||
{
|
||||
@ -190,17 +194,66 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
}
|
||||
|
||||
protected void ClickVisiblePanelWithOffset<T>(int index, Vector2 positionOffsetFromCentre)
|
||||
where T : Drawable
|
||||
{
|
||||
AddStep($"move mouse to panel {index} with offset {positionOffsetFromCentre}", () =>
|
||||
{
|
||||
var panel = Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
|
||||
.ChildrenOfType<T>()
|
||||
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
|
||||
.OrderBy(p => p.Y)
|
||||
.ElementAt(index);
|
||||
|
||||
InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre + panel.ToScreenSpace(positionOffsetFromCentre) - panel.ToScreenSpace(Vector2.Zero));
|
||||
});
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add requested beatmap sets count to list.
|
||||
/// </summary>
|
||||
/// <param name="count">The count of beatmap sets to add.</param>
|
||||
/// <param name="fixedDifficultiesPerSet">If not null, the number of difficulties per set. If null, randomised difficulty count will be used.</param>
|
||||
protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () =>
|
||||
/// <param name="randomMetadata">Whether to randomise the metadata to make groupings more uniform.</param>
|
||||
protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () =>
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)));
|
||||
BeatmapSets.Add(CreateTestBeatmapSetInfo(fixedDifficultiesPerSet, randomMetadata));
|
||||
});
|
||||
|
||||
protected static BeatmapSetInfo CreateTestBeatmapSetInfo(int? fixedDifficultiesPerSet, bool randomMetadata)
|
||||
{
|
||||
var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4));
|
||||
|
||||
if (randomMetadata)
|
||||
{
|
||||
char randomCharacter = getRandomCharacter();
|
||||
|
||||
var metadata = new BeatmapMetadata
|
||||
{
|
||||
// Create random metadata, then we can check if sorting works based on these
|
||||
Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9),
|
||||
Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}",
|
||||
Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) },
|
||||
};
|
||||
|
||||
foreach (var beatmap in beatmapSetInfo.Beatmaps)
|
||||
beatmap.Metadata = metadata.DeepClone();
|
||||
}
|
||||
|
||||
return beatmapSetInfo;
|
||||
}
|
||||
|
||||
private static long randomCharPointer;
|
||||
|
||||
private static char getRandomCharacter()
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*";
|
||||
return chars[(int)((randomCharPointer++ / 2) % chars.Length)];
|
||||
}
|
||||
|
||||
protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear());
|
||||
|
||||
protected void RemoveFirstBeatmap() =>
|
||||
|
@ -0,0 +1,91 @@
|
||||
// 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 System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
/// <summary>
|
||||
/// Covers common steps which can be used for manual testing.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapCarouselV2 : BeatmapCarouselV2TestScene
|
||||
{
|
||||
[Test]
|
||||
[Explicit]
|
||||
public void TestBasics()
|
||||
{
|
||||
CreateCarousel();
|
||||
RemoveAllBeatmaps();
|
||||
|
||||
AddBeatmaps(10, randomMetadata: true);
|
||||
AddBeatmaps(10);
|
||||
AddBeatmaps(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Explicit]
|
||||
public void TestSorting()
|
||||
{
|
||||
SortBy(new FilterCriteria { Sort = SortMode.Artist });
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Explicit]
|
||||
public void TestRemovals()
|
||||
{
|
||||
RemoveFirstBeatmap();
|
||||
RemoveAllBeatmaps();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Explicit]
|
||||
public void TestAddRemoveRepeatedOps()
|
||||
{
|
||||
AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20);
|
||||
AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Explicit]
|
||||
public void TestMasking()
|
||||
{
|
||||
AddStep("disable masking", () => Scroll.Masking = false);
|
||||
AddStep("enable masking", () => Scroll.Masking = true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Explicit]
|
||||
public void TestPerformanceWithManyBeatmaps()
|
||||
{
|
||||
const int count = 200000;
|
||||
|
||||
List<BeatmapSetInfo> generated = new List<BeatmapSetInfo>();
|
||||
|
||||
AddStep($"populate {count} test beatmaps", () =>
|
||||
{
|
||||
generated.Clear();
|
||||
Task.Run(() =>
|
||||
{
|
||||
for (int j = 0; j < count; j++)
|
||||
generated.Add(CreateTestBeatmapSetInfo(3, true));
|
||||
}).ConfigureAwait(true);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3));
|
||||
AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2));
|
||||
AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count));
|
||||
|
||||
AddStep("add all beatmaps", () => BeatmapSets.AddRange(generated));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
// 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.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapCarouselV2ArtistGrouping : BeatmapCarouselV2TestScene
|
||||
{
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist });
|
||||
|
||||
AddBeatmaps(10, 3, true);
|
||||
WaitForDrawablePanels();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionMouse()
|
||||
{
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
|
||||
AddUntilStep("some sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
|
||||
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionKeyboard()
|
||||
{
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
AddUntilStep("some sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
CheckNoSelection();
|
||||
|
||||
Select();
|
||||
|
||||
AddUntilStep("no sets visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
CheckNoSelection();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselRemembersSelection()
|
||||
{
|
||||
SelectNextGroup();
|
||||
|
||||
object? selection = null;
|
||||
|
||||
AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model);
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
|
||||
RemoveAllBeatmaps();
|
||||
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
|
||||
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGroupSelectionOnHeader()
|
||||
{
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
SelectPrevPanel();
|
||||
SelectPrevPanel();
|
||||
|
||||
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
|
||||
SelectPrevGroup();
|
||||
|
||||
WaitForGroupSelection(0, 1);
|
||||
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
|
||||
SelectPrevGroup();
|
||||
|
||||
WaitForGroupSelection(0, 1);
|
||||
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyboardSelection()
|
||||
{
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
CheckNoSelection();
|
||||
|
||||
// open first group
|
||||
Select();
|
||||
CheckNoSelection();
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapSetPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
WaitForGroupSelection(3, 1);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(3, 5);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(4, 1);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(3, 5);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(4, 1);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(4, 5);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
SelectNextPanel();
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(1, 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
// 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 System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
/// <summary>
|
||||
/// Currently covers adding and removing of items and scrolling.
|
||||
/// If we add more tests here, these two categories can likely be split out into separate scenes.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapCarouselV2Basics : BeatmapCarouselV2TestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestBasics()
|
||||
{
|
||||
AddBeatmaps(1);
|
||||
AddBeatmaps(10);
|
||||
RemoveFirstBeatmap();
|
||||
RemoveAllBeatmaps();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOffScreenLoading()
|
||||
{
|
||||
AddStep("disable masking", () => Scroll.Masking = false);
|
||||
AddStep("enable masking", () => Scroll.Masking = true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddRemoveOneByOne()
|
||||
{
|
||||
AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20);
|
||||
AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSorting()
|
||||
{
|
||||
AddBeatmaps(10);
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
||||
SortBy(new FilterCriteria { Sort = SortMode.Artist });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollPositionMaintainedOnAddSecondSelected()
|
||||
{
|
||||
Quad positionBefore = default;
|
||||
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2));
|
||||
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value)));
|
||||
|
||||
WaitForScrolling();
|
||||
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
|
||||
RemoveFirstBeatmap();
|
||||
WaitForSorting();
|
||||
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollPositionMaintainedOnAddLastSelected()
|
||||
{
|
||||
Quad positionBefore = default;
|
||||
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddStep("scroll to last item", () => Scroll.ScrollToEnd(false));
|
||||
|
||||
AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last());
|
||||
|
||||
WaitForScrolling();
|
||||
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
|
||||
RemoveFirstBeatmap();
|
||||
WaitForSorting();
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Explicit]
|
||||
public void TestPerformanceWithManyBeatmaps()
|
||||
{
|
||||
const int count = 200000;
|
||||
|
||||
List<BeatmapSetInfo> generated = new List<BeatmapSetInfo>();
|
||||
|
||||
AddStep($"populate {count} test beatmaps", () =>
|
||||
{
|
||||
generated.Clear();
|
||||
Task.Run(() =>
|
||||
{
|
||||
for (int j = 0; j < count; j++)
|
||||
generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)));
|
||||
}).ConfigureAwait(true);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3));
|
||||
AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2));
|
||||
AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count));
|
||||
|
||||
AddStep("add all beatmaps", () => BeatmapSets.AddRange(generated));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
// 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.Testing;
|
||||
@ -10,28 +9,26 @@ using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapCarouselV2GroupSelection : BeatmapCarouselV2TestScene
|
||||
public partial class TestSceneBeatmapCarouselV2DifficultyGrouping : BeatmapCarouselV2TestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
|
||||
CreateCarousel();
|
||||
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
||||
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionMouse()
|
||||
{
|
||||
AddBeatmaps(10, 5);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
@ -47,86 +44,99 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionKeyboard()
|
||||
{
|
||||
AddBeatmaps(10, 5);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
CheckNoSelection();
|
||||
|
||||
Select();
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
CheckNoSelection();
|
||||
|
||||
GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType<GroupPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselRemembersSelection()
|
||||
{
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
|
||||
object? selection = null;
|
||||
|
||||
AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model);
|
||||
AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model);
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
|
||||
RemoveAllBeatmaps();
|
||||
AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null);
|
||||
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
AddBeatmaps(10);
|
||||
AddBeatmaps(5);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("no drawable selection", getSelectedPanel, () => Is.Null);
|
||||
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
|
||||
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null);
|
||||
AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
|
||||
AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGroupSelectionOnHeader()
|
||||
public void TestGroupSelectionOnHeaderKeyboard()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
SelectPrevPanel();
|
||||
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(2, 9);
|
||||
|
||||
WaitForGroupSelection(0, 0);
|
||||
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
|
||||
SelectPrevGroup();
|
||||
|
||||
WaitForGroupSelection(0, 0);
|
||||
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGroupSelectionOnHeaderMouse()
|
||||
{
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf<BeatmapPanel>);
|
||||
AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf<BeatmapPanel>);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<GroupPanel>);
|
||||
AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf<GroupPanel>);
|
||||
AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
|
||||
AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf<BeatmapPanel>);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyboardSelection()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
@ -161,60 +171,24 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestInputHandlingWithinGaps()
|
||||
{
|
||||
AddBeatmaps(5, 2);
|
||||
WaitForDrawablePanels();
|
||||
SelectNextGroup();
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
|
||||
|
||||
clickOnPanel(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(p.LayoutRectangle.Centre.X, -1f));
|
||||
WaitForGroupSelection(0, 1);
|
||||
// Clicks just above the first group panel should not actuate any action.
|
||||
ClickVisiblePanelWithOffset<GroupPanel>(0, new Vector2(0, -(GroupPanel.HEIGHT / 2 + 1)));
|
||||
|
||||
clickOnPanel(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(p.LayoutRectangle.Centre.X, 1f));
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
|
||||
|
||||
ClickVisiblePanelWithOffset<GroupPanel>(0, new Vector2(0, -(GroupPanel.HEIGHT / 2)));
|
||||
|
||||
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<BeatmapPanel>().Any());
|
||||
CheckNoSelection();
|
||||
|
||||
// Beatmap panels expand their selection area to cover holes from spacing.
|
||||
ClickVisiblePanelWithOffset<BeatmapPanel>(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
ClickVisiblePanelWithOffset<BeatmapPanel>(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
clickOnGroup(0, p => p.LayoutRectangle.BottomLeft + new Vector2(p.LayoutRectangle.Centre.X, 1f));
|
||||
AddAssert("group 0 collapsed", () => this.ChildrenOfType<GroupPanel>().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.False);
|
||||
clickOnGroup(0, p => p.LayoutRectangle.Centre);
|
||||
AddAssert("group 0 expanded", () => this.ChildrenOfType<GroupPanel>().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.True);
|
||||
|
||||
AddStep("scroll to end", () => Scroll.ScrollToEnd(false));
|
||||
clickOnPanel(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(p.LayoutRectangle.Centre.X, 1f));
|
||||
WaitForGroupSelection(0, 4);
|
||||
|
||||
clickOnGroup(1, p => p.LayoutRectangle.TopLeft + new Vector2(p.LayoutRectangle.Centre.X, -1f));
|
||||
AddAssert("group 1 expanded", () => this.ChildrenOfType<GroupPanel>().OrderBy(g => g.Y).ElementAt(1).Expanded.Value, () => Is.True);
|
||||
}
|
||||
|
||||
private void clickOnGroup(int group, Func<GroupPanel, Vector2> pos)
|
||||
{
|
||||
AddStep($"click on group{group}", () =>
|
||||
{
|
||||
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
|
||||
var model = groupingFilter.GroupItems.Keys.ElementAt(group);
|
||||
|
||||
var panel = this.ChildrenOfType<GroupPanel>().Single(b => ReferenceEquals(b.Item!.Model, model));
|
||||
InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel)));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
}
|
||||
|
||||
private void clickOnPanel(int group, int panel, Func<BeatmapPanel, Vector2> pos)
|
||||
{
|
||||
AddStep($"click on group{group} panel{panel}", () =>
|
||||
{
|
||||
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
|
||||
|
||||
var g = groupingFilter.GroupItems.Keys.ElementAt(group);
|
||||
// offset by one because the group itself is included in the items list.
|
||||
object model = groupingFilter.GroupItems[g].ElementAt(panel + 1).Model;
|
||||
|
||||
var p = this.ChildrenOfType<BeatmapPanel>().Single(b => ReferenceEquals(b.Item!.Model, model));
|
||||
InputManager.MoveMouseTo(p.ToScreenSpace(pos(p)));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
// 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.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -13,8 +14,16 @@ using osuTK.Input;
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapCarouselV2Selection : BeatmapCarouselV2TestScene
|
||||
public partial class TestSceneBeatmapCarouselV2NoGrouping : BeatmapCarouselV2TestScene
|
||||
{
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
SortBy(new FilterCriteria { Sort = SortMode.Title });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keyboard selection via up and down arrows doesn't actually change the selection until
|
||||
/// the select key is pressed.
|
||||
@ -79,27 +88,26 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
object? selection = null;
|
||||
|
||||
AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model);
|
||||
AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model);
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
|
||||
RemoveAllBeatmaps();
|
||||
AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null);
|
||||
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("no drawable selection", getSelectedPanel, () => Is.Null);
|
||||
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
|
||||
|
||||
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
|
||||
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -130,6 +138,25 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
WaitForSelection(0, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGroupSelectionOnHeader()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
SelectNextGroup();
|
||||
WaitForSelection(1, 0);
|
||||
|
||||
SelectPrevPanel();
|
||||
SelectPrevGroup();
|
||||
WaitForSelection(1, 0);
|
||||
|
||||
SelectPrevPanel();
|
||||
SelectNextGroup();
|
||||
WaitForSelection(1, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyboardSelection()
|
||||
{
|
||||
@ -185,26 +212,29 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
AddBeatmaps(2, 5);
|
||||
WaitForDrawablePanels();
|
||||
SelectNextGroup();
|
||||
|
||||
clickOnDifficulty(0, 1, p => new Vector2(p.LayoutRectangle.Centre.X, -1f));
|
||||
WaitForSelection(0, 1);
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
|
||||
|
||||
clickOnDifficulty(0, 0, p => new Vector2(p.LayoutRectangle.Centre.X, BeatmapPanel.HEIGHT + 1f));
|
||||
// Clicks just above the first group panel should not actuate any action.
|
||||
ClickVisiblePanelWithOffset<BeatmapSetPanel>(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2 + 1)));
|
||||
|
||||
AddAssert("no beatmaps visible", () => !GetVisiblePanels<BeatmapPanel>().Any());
|
||||
|
||||
ClickVisiblePanelWithOffset<BeatmapSetPanel>(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2)));
|
||||
|
||||
AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels<BeatmapPanel>().Any());
|
||||
WaitForSelection(0, 0);
|
||||
|
||||
clickOnDifficulty(0, 1, p => p.LayoutRectangle.Centre);
|
||||
WaitForSelection(0, 1);
|
||||
|
||||
clickOnSet(0, p => new Vector2(p.LayoutRectangle.Centre.X, BeatmapSetPanel.HEIGHT + 1f));
|
||||
// Beatmap panels expand their selection area to cover holes from spacing.
|
||||
ClickVisiblePanelWithOffset<BeatmapPanel>(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForSelection(0, 0);
|
||||
|
||||
AddStep("scroll to end", () => Scroll.ScrollToEnd(false));
|
||||
clickOnDifficulty(0, 4, p => new Vector2(p.LayoutRectangle.Centre.X, BeatmapPanel.HEIGHT + 1f));
|
||||
WaitForSelection(0, 4);
|
||||
// Panels with higher depth will handle clicks in the gutters for simplicity.
|
||||
ClickVisiblePanelWithOffset<BeatmapPanel>(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForSelection(0, 2);
|
||||
|
||||
clickOnSet(1, p => new Vector2(p.LayoutRectangle.Centre.X, -1f));
|
||||
WaitForSelection(1, 0);
|
||||
ClickVisiblePanelWithOffset<BeatmapPanel>(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1)));
|
||||
WaitForSelection(0, 3);
|
||||
}
|
||||
|
||||
private void checkSelectionIterating(bool isIterating)
|
||||
@ -220,27 +250,5 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection);
|
||||
}
|
||||
}
|
||||
|
||||
private void clickOnSet(int set, Func<BeatmapSetPanel, Vector2> pos)
|
||||
{
|
||||
AddStep($"click on set{set}", () =>
|
||||
{
|
||||
var model = BeatmapSets[set];
|
||||
var panel = this.ChildrenOfType<BeatmapSetPanel>().Single(b => ReferenceEquals(b.Item!.Model, model));
|
||||
InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel)));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
}
|
||||
|
||||
private void clickOnDifficulty(int set, int diff, Func<BeatmapPanel, Vector2> pos)
|
||||
{
|
||||
AddStep($"click on set{set} diff{diff}", () =>
|
||||
{
|
||||
var model = BeatmapSets[set].Beatmaps[diff];
|
||||
var panel = this.ChildrenOfType<BeatmapPanel>().Single(b => ReferenceEquals(b.Item!.Model, model));
|
||||
InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel)));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapCarouselV2Scrolling : BeatmapCarouselV2TestScene
|
||||
{
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
SortBy(new FilterCriteria());
|
||||
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollPositionMaintainedOnAddSecondSelected()
|
||||
{
|
||||
Quad positionBefore = default;
|
||||
|
||||
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First());
|
||||
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value)));
|
||||
|
||||
WaitForScrolling();
|
||||
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
|
||||
RemoveFirstBeatmap();
|
||||
WaitForSorting();
|
||||
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollPositionMaintainedOnAddLastSelected()
|
||||
{
|
||||
Quad positionBefore = default;
|
||||
|
||||
AddStep("scroll to last item", () => Scroll.ScrollToEnd(false));
|
||||
|
||||
AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last().Beatmaps.Last());
|
||||
|
||||
WaitForScrolling();
|
||||
|
||||
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
|
||||
|
||||
RemoveFirstBeatmap();
|
||||
WaitForSorting();
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
}
|
||||
}
|
||||
}
|
@ -1239,7 +1239,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Solo]
|
||||
public void TestHardDeleteHandledCorrectly()
|
||||
{
|
||||
createSongSelect();
|
||||
|
@ -31,49 +31,49 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
new GroupPanel
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition("Group A"))
|
||||
Item = new CarouselItem(new GroupDefinition('A', "Group A"))
|
||||
},
|
||||
new GroupPanel
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition("Group A")),
|
||||
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
|
||||
KeyboardSelected = { Value = true }
|
||||
},
|
||||
new GroupPanel
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition("Group A")),
|
||||
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
|
||||
Expanded = { Value = true }
|
||||
},
|
||||
new GroupPanel
|
||||
{
|
||||
Item = new CarouselItem(new GroupDefinition("Group A")),
|
||||
Item = new CarouselItem(new GroupDefinition('A', "Group A")),
|
||||
KeyboardSelected = { Value = true },
|
||||
Expanded = { Value = true }
|
||||
},
|
||||
new StarsGroupPanel
|
||||
{
|
||||
Item = new CarouselItem(new StarsGroupDefinition(1))
|
||||
Item = new CarouselItem(new GroupDefinition(1, "1"))
|
||||
},
|
||||
new StarsGroupPanel
|
||||
{
|
||||
Item = new CarouselItem(new StarsGroupDefinition(3)),
|
||||
Item = new CarouselItem(new GroupDefinition(3, "3")),
|
||||
Expanded = { Value = true }
|
||||
},
|
||||
new StarsGroupPanel
|
||||
{
|
||||
Item = new CarouselItem(new StarsGroupDefinition(5)),
|
||||
Item = new CarouselItem(new GroupDefinition(5, "5")),
|
||||
},
|
||||
new StarsGroupPanel
|
||||
{
|
||||
Item = new CarouselItem(new StarsGroupDefinition(7)),
|
||||
Item = new CarouselItem(new GroupDefinition(7, "7")),
|
||||
Expanded = { Value = true }
|
||||
},
|
||||
new StarsGroupPanel
|
||||
{
|
||||
Item = new CarouselItem(new StarsGroupDefinition(8)),
|
||||
Item = new CarouselItem(new GroupDefinition(8, "8")),
|
||||
},
|
||||
new StarsGroupPanel
|
||||
{
|
||||
Item = new CarouselItem(new StarsGroupDefinition(9)),
|
||||
Item = new CarouselItem(new GroupDefinition(9, "9")),
|
||||
Expanded = { Value = true }
|
||||
},
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps
|
||||
if (beatmapInfo.OnlineID <= 0 || beatmapInfo.BeatmapSet == null)
|
||||
return null;
|
||||
|
||||
return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}";
|
||||
return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,9 +41,9 @@ namespace osu.Game.Beatmaps
|
||||
return null;
|
||||
|
||||
if (ruleset != null)
|
||||
return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}";
|
||||
return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}";
|
||||
|
||||
return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}";
|
||||
return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetInfo.OnlineID}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +95,9 @@ namespace osu.Game.Configuration
|
||||
/// </summary>
|
||||
DailyChallengeIntroPlayed,
|
||||
|
||||
/// <summary>
|
||||
/// The activity for the current user to broadcast to other players.
|
||||
/// </summary>
|
||||
UserOnlineActivity,
|
||||
}
|
||||
}
|
||||
|
@ -40,9 +40,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
private readonly Queue<APIRequest> queue = new Queue<APIRequest>();
|
||||
|
||||
public string APIEndpointUrl { get; }
|
||||
|
||||
public string WebsiteRootUrl { get; }
|
||||
public EndpointConfiguration Endpoints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The API response version.
|
||||
@ -75,7 +73,7 @@ namespace osu.Game.Online.API
|
||||
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
|
||||
private readonly Logger log;
|
||||
|
||||
public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash)
|
||||
public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpoints, string versionHash)
|
||||
{
|
||||
this.game = game;
|
||||
this.config = config;
|
||||
@ -89,14 +87,13 @@ namespace osu.Game.Online.API
|
||||
APIVersion = now.Year * 10000 + now.Month * 100 + now.Day;
|
||||
}
|
||||
|
||||
APIEndpointUrl = endpointConfiguration.APIEndpointUrl;
|
||||
WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl;
|
||||
Endpoints = endpoints;
|
||||
NotificationsClient = setUpNotificationsClient();
|
||||
|
||||
authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl);
|
||||
authentication = new OAuth(endpoints.APIClientID, endpoints.APIClientSecret, Endpoints.APIUrl);
|
||||
|
||||
log = Logger.GetLogger(LoggingTarget.Network);
|
||||
log.Add($@"API endpoint root: {APIEndpointUrl}");
|
||||
log.Add($@"API endpoint root: {Endpoints.APIUrl}");
|
||||
log.Add($@"API request version: {APIVersion}");
|
||||
|
||||
ProvidedUsername = config.Get<string>(OsuSetting.Username);
|
||||
@ -408,7 +405,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
var req = new RegistrationRequest
|
||||
{
|
||||
Url = $@"{APIEndpointUrl}/users",
|
||||
Url = $@"{Endpoints.APIUrl}/users",
|
||||
Method = HttpMethod.Post,
|
||||
Username = username,
|
||||
Email = email,
|
||||
|
@ -71,7 +71,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri);
|
||||
|
||||
protected virtual string Uri => $@"{API!.APIEndpointUrl}/api/v2/{Target}";
|
||||
protected virtual string Uri => $@"{API!.Endpoints.APIUrl}/api/v2/{Target}";
|
||||
|
||||
protected IAPIProvider? API;
|
||||
|
||||
|
@ -41,9 +41,11 @@ namespace osu.Game.Online.API
|
||||
|
||||
public string ProvidedUsername => LocalUser.Value.Username;
|
||||
|
||||
public string APIEndpointUrl => "http://localhost";
|
||||
|
||||
public string WebsiteRootUrl => "http://localhost";
|
||||
public EndpointConfiguration Endpoints { get; } = new EndpointConfiguration
|
||||
{
|
||||
APIUrl = "http://localhost",
|
||||
WebsiteUrl = "http://localhost",
|
||||
};
|
||||
|
||||
public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd"));
|
||||
|
||||
|
@ -51,14 +51,9 @@ namespace osu.Game.Online.API
|
||||
string ProvidedUsername { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL endpoint for this API. Does not include a trailing slash.
|
||||
/// Holds configuration for online endpoints.
|
||||
/// </summary>
|
||||
string APIEndpointUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The root URL of the website, excluding the trailing slash.
|
||||
/// </summary>
|
||||
string WebsiteRootUrl { get; }
|
||||
EndpointConfiguration Endpoints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of the API.
|
||||
|
26
osu.Game/Online/API/Requests/APIUploadRequest.cs
Normal file
26
osu.Game/Online/API/Requests/APIUploadRequest.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.IO.Network;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public abstract class APIUploadRequest : APIRequest
|
||||
{
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var request = base.CreateWebRequest();
|
||||
request.UploadProgress += onUploadProgress;
|
||||
return request;
|
||||
}
|
||||
|
||||
private void onUploadProgress(long current, long total)
|
||||
{
|
||||
Debug.Assert(API != null);
|
||||
API.Schedule(() => Progressed?.Invoke(current, total));
|
||||
}
|
||||
|
||||
public event APIProgressHandler? Progressed;
|
||||
}
|
||||
}
|
55
osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs
Normal file
55
osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs
Normal file
@ -0,0 +1,55 @@
|
||||
// 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.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class PatchBeatmapPackageRequest : APIUploadRequest
|
||||
{
|
||||
protected override string Uri
|
||||
{
|
||||
get
|
||||
{
|
||||
// can be removed once the service has been successfully deployed to production
|
||||
if (API!.Endpoints.BeatmapSubmissionServiceUrl == null)
|
||||
throw new NotSupportedException("Beatmap submission not supported in this configuration!");
|
||||
|
||||
return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}";
|
||||
}
|
||||
}
|
||||
|
||||
protected override string Target => throw new NotSupportedException();
|
||||
|
||||
public uint BeatmapSetID { get; }
|
||||
|
||||
// ReSharper disable once CollectionNeverUpdated.Global
|
||||
public Dictionary<string, byte[]> FilesChanged { get; } = new Dictionary<string, byte[]>();
|
||||
|
||||
// ReSharper disable once CollectionNeverUpdated.Global
|
||||
public HashSet<string> FilesDeleted { get; } = new HashSet<string>();
|
||||
|
||||
public PatchBeatmapPackageRequest(uint beatmapSetId)
|
||||
{
|
||||
BeatmapSetID = beatmapSetId;
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var request = base.CreateWebRequest();
|
||||
request.Method = HttpMethod.Patch;
|
||||
|
||||
foreach ((string filename, byte[] content) in FilesChanged)
|
||||
request.AddFile(@"filesChanged", content, filename);
|
||||
|
||||
foreach (string filename in FilesDeleted)
|
||||
request.AddParameter(@"filesDeleted", filename, RequestParameterType.Form);
|
||||
|
||||
request.Timeout = 60_000;
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
82
osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs
Normal file
82
osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class PutBeatmapSetRequest : APIRequest<PutBeatmapSetResponse>
|
||||
{
|
||||
protected override string Uri
|
||||
{
|
||||
get
|
||||
{
|
||||
// can be removed once the service has been successfully deployed to production
|
||||
if (API!.Endpoints.BeatmapSubmissionServiceUrl == null)
|
||||
throw new NotSupportedException("Beatmap submission not supported in this configuration!");
|
||||
|
||||
return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl}/beatmapsets";
|
||||
}
|
||||
}
|
||||
|
||||
protected override string Target => throw new NotSupportedException();
|
||||
|
||||
[JsonProperty("beatmapset_id")]
|
||||
public uint? BeatmapSetID { get; init; }
|
||||
|
||||
[JsonProperty("beatmaps_to_create")]
|
||||
public uint BeatmapsToCreate { get; init; }
|
||||
|
||||
[JsonProperty("beatmaps_to_keep")]
|
||||
public uint[] BeatmapsToKeep { get; init; } = [];
|
||||
|
||||
[JsonProperty("target")]
|
||||
public BeatmapSubmissionTarget SubmissionTarget { get; init; }
|
||||
|
||||
private PutBeatmapSetRequest()
|
||||
{
|
||||
}
|
||||
|
||||
public static PutBeatmapSetRequest CreateNew(uint beatmapCount, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest
|
||||
{
|
||||
BeatmapsToCreate = beatmapCount,
|
||||
SubmissionTarget = target,
|
||||
};
|
||||
|
||||
public static PutBeatmapSetRequest UpdateExisting(uint beatmapSetId, IEnumerable<uint> beatmapsToKeep, uint beatmapsToCreate, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest
|
||||
{
|
||||
BeatmapSetID = beatmapSetId,
|
||||
BeatmapsToKeep = beatmapsToKeep.ToArray(),
|
||||
BeatmapsToCreate = beatmapsToCreate,
|
||||
SubmissionTarget = target,
|
||||
};
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var req = base.CreateWebRequest();
|
||||
req.Method = HttpMethod.Put;
|
||||
req.ContentType = @"application/json";
|
||||
req.AddRaw(JsonConvert.SerializeObject(this));
|
||||
return req;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum BeatmapSubmissionTarget
|
||||
{
|
||||
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))]
|
||||
WIP,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))]
|
||||
Pending,
|
||||
}
|
||||
}
|
45
osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs
Normal file
45
osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs
Normal file
@ -0,0 +1,45 @@
|
||||
// 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.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class ReplaceBeatmapPackageRequest : APIUploadRequest
|
||||
{
|
||||
protected override string Uri
|
||||
{
|
||||
get
|
||||
{
|
||||
// can be removed once the service has been successfully deployed to production
|
||||
if (API!.Endpoints.BeatmapSubmissionServiceUrl == null)
|
||||
throw new NotSupportedException("Beatmap submission not supported in this configuration!");
|
||||
|
||||
return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}";
|
||||
}
|
||||
}
|
||||
|
||||
protected override string Target => throw new NotSupportedException();
|
||||
|
||||
public uint BeatmapSetID { get; }
|
||||
|
||||
private readonly byte[] oszPackage;
|
||||
|
||||
public ReplaceBeatmapPackageRequest(uint beatmapSetID, byte[] oszPackage)
|
||||
{
|
||||
this.oszPackage = oszPackage;
|
||||
BeatmapSetID = beatmapSetID;
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var request = base.CreateWebRequest();
|
||||
request.AddFile(@"beatmapArchive", oszPackage);
|
||||
request.Method = HttpMethod.Put;
|
||||
request.Timeout = 60_000;
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
{
|
||||
public class PutBeatmapSetResponse
|
||||
{
|
||||
[JsonProperty("beatmapset_id")]
|
||||
public uint BeatmapSetId { get; set; }
|
||||
|
||||
[JsonProperty("beatmap_ids")]
|
||||
public ICollection<uint> BeatmapIds { get; set; } = Array.Empty<uint>();
|
||||
|
||||
[JsonProperty("files")]
|
||||
public ICollection<BeatmapSetFile> Files { get; set; } = Array.Empty<BeatmapSetFile>();
|
||||
}
|
||||
|
||||
public struct BeatmapSetFile
|
||||
{
|
||||
[JsonProperty("filename")]
|
||||
public string Filename { get; set; }
|
||||
|
||||
[JsonProperty("sha2_hash")]
|
||||
public string SHA2Hash { get; set; }
|
||||
}
|
||||
}
|
@ -49,12 +49,12 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
if (url.StartsWith('/'))
|
||||
{
|
||||
url = $"{api.WebsiteRootUrl}{url}";
|
||||
url = $"{api.Endpoints.WebsiteUrl}{url}";
|
||||
isTrustedDomain = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isTrustedDomain = url.StartsWith(api.WebsiteRootUrl, StringComparison.Ordinal);
|
||||
isTrustedDomain = url.StartsWith(api.Endpoints.WebsiteUrl, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (!url.CheckIsValidUrl())
|
||||
|
@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
string getBeatmapPart()
|
||||
{
|
||||
return beatmapOnlineID > 0 ? $"[{api.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle;
|
||||
return beatmapOnlineID > 0 ? $"[{api.Endpoints.WebsiteUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle;
|
||||
}
|
||||
|
||||
string getRulesetPart()
|
||||
|
@ -7,12 +7,12 @@ namespace osu.Game.Online
|
||||
{
|
||||
public DevelopmentEndpointConfiguration()
|
||||
{
|
||||
WebsiteRootUrl = APIEndpointUrl = @"https://dev.ppy.sh";
|
||||
WebsiteUrl = APIUrl = @"https://dev.ppy.sh";
|
||||
APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT";
|
||||
APIClientID = "5";
|
||||
SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator";
|
||||
MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer";
|
||||
MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata";
|
||||
SpectatorUrl = $@"{APIUrl}/signalr/spectator";
|
||||
MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer";
|
||||
MetadataUrl = $@"{APIUrl}/signalr/metadata";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,16 +8,6 @@ namespace osu.Game.Online
|
||||
/// </summary>
|
||||
public class EndpointConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The base URL for the website.
|
||||
/// </summary>
|
||||
public string WebsiteRootUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The endpoint for the main (osu-web) API.
|
||||
/// </summary>
|
||||
public string APIEndpointUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The OAuth client secret.
|
||||
/// </summary>
|
||||
@ -28,19 +18,34 @@ namespace osu.Game.Online
|
||||
/// </summary>
|
||||
public string APIClientID { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The base URL for the website. Does not include a trailing slash.
|
||||
/// </summary>
|
||||
public string WebsiteUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The endpoint for the main (osu-web) API. Does not include a trailing slash.
|
||||
/// </summary>
|
||||
public string APIUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The root URL for the service handling beatmap submission. Does not include a trailing slash.
|
||||
/// </summary>
|
||||
public string? BeatmapSubmissionServiceUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The endpoint for the SignalR spectator server.
|
||||
/// </summary>
|
||||
public string SpectatorEndpointUrl { get; set; } = string.Empty;
|
||||
public string SpectatorUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The endpoint for the SignalR multiplayer server.
|
||||
/// </summary>
|
||||
public string MultiplayerEndpointUrl { get; set; } = string.Empty;
|
||||
public string MultiplayerUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The endpoint for the SignalR metadata server.
|
||||
/// </summary>
|
||||
public string MetadataEndpointUrl { get; set; } = string.Empty;
|
||||
public string MetadataUrl { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
@ -436,7 +436,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
|
||||
|
||||
if (Score.OnlineID > 0)
|
||||
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.WebsiteRootUrl}/scores/{Score.OnlineID}")));
|
||||
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}")));
|
||||
|
||||
if (Score.Files.Count > 0)
|
||||
{
|
||||
|
@ -3,9 +3,13 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Online.Metadata
|
||||
@ -14,6 +18,9 @@ namespace osu.Game.Online.Metadata
|
||||
{
|
||||
public abstract IBindable<bool> IsConnected { get; }
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
#region Beatmap metadata updates
|
||||
|
||||
public abstract Task<BeatmapUpdates> GetChangesSince(int queueId);
|
||||
@ -32,11 +39,6 @@ namespace osu.Game.Online.Metadata
|
||||
|
||||
#region User presence updates
|
||||
|
||||
/// <summary>
|
||||
/// Whether the client is currently receiving user presence updates from the server.
|
||||
/// </summary>
|
||||
public abstract IBindable<bool> IsWatchingUserPresence { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="UserPresence"/> information about the current user.
|
||||
/// </summary>
|
||||
@ -52,31 +54,91 @@ namespace osu.Game.Online.Metadata
|
||||
/// </summary>
|
||||
public abstract IBindableDictionary<int, UserPresence> FriendPresences { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the presence of a user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user ID.</param>
|
||||
/// <returns>The user presence, or null if not available or the user's offline.</returns>
|
||||
public UserPresence? GetPresence(int userId)
|
||||
{
|
||||
if (userId == api.LocalUser.Value.OnlineID)
|
||||
return LocalUserPresence;
|
||||
|
||||
if (FriendPresences.TryGetValue(userId, out UserPresence presence))
|
||||
return presence;
|
||||
|
||||
if (UserPresences.TryGetValue(userId, out presence))
|
||||
return presence;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public abstract Task UpdateActivity(UserActivity? activity);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task UpdateStatus(UserStatus? status);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task BeginWatchingUserPresence();
|
||||
private int userPresenceWatchCount;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task EndWatchingUserPresence();
|
||||
protected bool IsWatchingUserPresence
|
||||
=> Interlocked.CompareExchange(ref userPresenceWatchCount, userPresenceWatchCount, userPresenceWatchCount) > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Signals to the server that we want to begin receiving status updates for all users.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IDisposable"/> which will end the session when disposed.</returns>
|
||||
public IDisposable BeginWatchingUserPresence() => new UserPresenceWatchToken(this);
|
||||
|
||||
Task IMetadataServer.BeginWatchingUserPresence()
|
||||
{
|
||||
if (Interlocked.Increment(ref userPresenceWatchCount) == 1)
|
||||
return BeginWatchingUserPresenceInternal();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IMetadataServer.EndWatchingUserPresence()
|
||||
{
|
||||
if (Interlocked.Decrement(ref userPresenceWatchCount) == 0)
|
||||
return EndWatchingUserPresenceInternal();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected abstract Task BeginWatchingUserPresenceInternal();
|
||||
|
||||
protected abstract Task EndWatchingUserPresenceInternal();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task UserPresenceUpdated(int userId, UserPresence? presence);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence);
|
||||
|
||||
private class UserPresenceWatchToken : IDisposable
|
||||
{
|
||||
private readonly IMetadataServer server;
|
||||
private bool isDisposed;
|
||||
|
||||
public UserPresenceWatchToken(IMetadataServer server)
|
||||
{
|
||||
this.server = server;
|
||||
server.BeginWatchingUserPresence().FireAndForget();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (isDisposed)
|
||||
return;
|
||||
|
||||
server.EndWatchingUserPresence().FireAndForget();
|
||||
isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Daily Challenge
|
||||
|
||||
public abstract IBindable<DailyChallengeInfo?> DailyChallengeInfo { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info);
|
||||
|
||||
#endregion
|
||||
|
@ -20,9 +20,6 @@ namespace osu.Game.Online.Metadata
|
||||
{
|
||||
public override IBindable<bool> IsConnected { get; } = new Bindable<bool>();
|
||||
|
||||
public override IBindable<bool> IsWatchingUserPresence => isWatchingUserPresence;
|
||||
private readonly BindableBool isWatchingUserPresence = new BindableBool();
|
||||
|
||||
public override UserPresence LocalUserPresence => localUserPresence;
|
||||
private UserPresence localUserPresence;
|
||||
|
||||
@ -50,7 +47,7 @@ namespace osu.Game.Online.Metadata
|
||||
|
||||
public OnlineMetadataClient(EndpointConfiguration endpoints)
|
||||
{
|
||||
endpoint = endpoints.MetadataEndpointUrl;
|
||||
endpoint = endpoints.MetadataUrl;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -109,15 +106,18 @@ namespace osu.Game.Online.Metadata
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
isWatchingUserPresence.Value = false;
|
||||
userPresences.Clear();
|
||||
friendPresences.Clear();
|
||||
dailyChallengeInfo.Value = null;
|
||||
localUserPresence = default;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsWatchingUserPresence)
|
||||
BeginWatchingUserPresenceInternal();
|
||||
|
||||
if (localUser.Value is not GuestUser)
|
||||
{
|
||||
UpdateActivity(userActivity.Value);
|
||||
@ -201,6 +201,31 @@ namespace osu.Game.Online.Metadata
|
||||
return connection.InvokeAsync(nameof(IMetadataServer.UpdateStatus), status);
|
||||
}
|
||||
|
||||
protected override Task BeginWatchingUserPresenceInternal()
|
||||
{
|
||||
if (connector?.IsConnected.Value != true)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network);
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
return connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence));
|
||||
}
|
||||
|
||||
protected override Task EndWatchingUserPresenceInternal()
|
||||
{
|
||||
if (connector?.IsConnected.Value != true)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network);
|
||||
|
||||
// must be scheduled before any remote calls to avoid mis-ordering.
|
||||
Schedule(() => userPresences.Clear());
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
return connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence));
|
||||
}
|
||||
|
||||
public override Task UserPresenceUpdated(int userId, UserPresence? presence)
|
||||
{
|
||||
Schedule(() =>
|
||||
@ -237,36 +262,6 @@ namespace osu.Game.Online.Metadata
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task BeginWatchingUserPresence()
|
||||
{
|
||||
if (connector?.IsConnected.Value != true)
|
||||
throw new OperationCanceledException();
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false);
|
||||
Schedule(() => isWatchingUserPresence.Value = true);
|
||||
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network);
|
||||
}
|
||||
|
||||
public override async Task EndWatchingUserPresence()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (connector?.IsConnected.Value != true)
|
||||
throw new OperationCanceledException();
|
||||
|
||||
// must be scheduled before any remote calls to avoid mis-ordering.
|
||||
Schedule(() => userPresences.Clear());
|
||||
Debug.Assert(connection != null);
|
||||
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false);
|
||||
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Schedule(() => isWatchingUserPresence.Value = false);
|
||||
}
|
||||
}
|
||||
|
||||
public override Task DailyChallengeUpdated(DailyChallengeInfo? info)
|
||||
{
|
||||
Schedule(() => dailyChallengeInfo.Value = info);
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
public OnlineMultiplayerClient(EndpointConfiguration endpoints)
|
||||
{
|
||||
endpoint = endpoints.MultiplayerEndpointUrl;
|
||||
endpoint = endpoints.MultiplayerUrl;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -7,12 +7,12 @@ namespace osu.Game.Online
|
||||
{
|
||||
public ProductionEndpointConfiguration()
|
||||
{
|
||||
WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh";
|
||||
WebsiteUrl = APIUrl = @"https://osu.ppy.sh";
|
||||
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
|
||||
APIClientID = "5";
|
||||
SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";
|
||||
MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer";
|
||||
MetadataEndpointUrl = "https://spectator.ppy.sh/metadata";
|
||||
SpectatorUrl = "https://spectator.ppy.sh/spectator";
|
||||
MultiplayerUrl = "https://spectator.ppy.sh/multiplayer";
|
||||
MetadataUrl = "https://spectator.ppy.sh/metadata";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Online.Spectator
|
||||
|
||||
public OnlineSpectatorClient(EndpointConfiguration endpoints)
|
||||
{
|
||||
endpoint = endpoints.SpectatorEndpointUrl;
|
||||
endpoint = endpoints.SpectatorUrl;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -295,7 +295,7 @@ namespace osu.Game
|
||||
|
||||
EndpointConfiguration endpoints = CreateEndpoints();
|
||||
|
||||
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;
|
||||
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteUrl;
|
||||
|
||||
frameworkLocale = frameworkConfig.GetBindable<string>(FrameworkSetting.Locale);
|
||||
frameworkLocale.BindValueChanged(_ => updateLanguage());
|
||||
|
@ -419,7 +419,7 @@ namespace osu.Game.Overlays.Comments
|
||||
|
||||
private void copyUrl()
|
||||
{
|
||||
clipboard.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}");
|
||||
clipboard.SetText($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}");
|
||||
onScreenDisplay?.Display(new CopyUrlToast());
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
@ -40,17 +38,20 @@ namespace osu.Game.Overlays.Dashboard
|
||||
private readonly IBindableDictionary<int, UserPresence> onlineUserPresences = new BindableDictionary<int, UserPresence>();
|
||||
private readonly Dictionary<int, OnlineUserPanel> userPanels = new Dictionary<int, OnlineUserPanel>();
|
||||
|
||||
private SearchContainer<OnlineUserPanel> userFlow;
|
||||
private BasicSearchTextBox searchTextBox;
|
||||
private SearchContainer<OnlineUserPanel> userFlow = null!;
|
||||
private BasicSearchTextBox searchTextBox = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private SpectatorClient spectatorClient { get; set; }
|
||||
private SpectatorClient spectatorClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MetadataClient metadataClient { get; set; }
|
||||
private MetadataClient metadataClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache users { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
@ -99,9 +100,6 @@ namespace osu.Game.Overlays.Dashboard
|
||||
searchTextBox.Current.ValueChanged += text => userFlow.SearchTerm = text.NewValue;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache users { get; set; }
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -120,7 +118,7 @@ namespace osu.Game.Overlays.Dashboard
|
||||
searchTextBox.TakeFocus();
|
||||
}
|
||||
|
||||
private void onUserPresenceUpdated(object sender, NotifyDictionaryChangedEventArgs<int, UserPresence> e) => Schedule(() =>
|
||||
private void onUserPresenceUpdated(object? sender, NotifyDictionaryChangedEventArgs<int, UserPresence> e) => Schedule(() =>
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
@ -133,36 +131,9 @@ namespace osu.Game.Overlays.Dashboard
|
||||
|
||||
users.GetUserAsync(userId).ContinueWith(task =>
|
||||
{
|
||||
APIUser user = task.GetResultSafely();
|
||||
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
userFlow.Add(userPanels[userId] = createUserPanel(user).With(p =>
|
||||
{
|
||||
var presence = onlineUserPresences.GetValueOrDefault(userId);
|
||||
|
||||
p.Status.Value = presence.Status;
|
||||
p.Activity.Value = presence.Activity;
|
||||
}));
|
||||
if (task.GetResultSafely() is APIUser user)
|
||||
Schedule(() => userFlow.Add(userPanels[userId] = createUserPanel(user)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyDictionaryChangedAction.Replace:
|
||||
Debug.Assert(e.NewItems != null);
|
||||
|
||||
foreach (var kvp in e.NewItems)
|
||||
{
|
||||
if (userPanels.TryGetValue(kvp.Key, out var panel))
|
||||
{
|
||||
panel.Activity.Value = kvp.Value.Activity;
|
||||
panel.Status.Value = kvp.Value.Status;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
@ -181,7 +152,7 @@ namespace osu.Game.Overlays.Dashboard
|
||||
}
|
||||
});
|
||||
|
||||
private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
private void onPlayingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
@ -221,15 +192,12 @@ namespace osu.Game.Overlays.Dashboard
|
||||
{
|
||||
public readonly APIUser User;
|
||||
|
||||
public readonly Bindable<UserStatus?> Status = new Bindable<UserStatus?>();
|
||||
public readonly Bindable<UserActivity> Activity = new Bindable<UserActivity>();
|
||||
|
||||
public BindableBool CanSpectate { get; } = new BindableBool();
|
||||
|
||||
public IEnumerable<LocalisableString> FilterTerms { get; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IPerformFromScreenRunner performer { get; set; }
|
||||
[Resolved]
|
||||
private IPerformFromScreenRunner? performer { get; set; }
|
||||
|
||||
public bool FilteringActive { set; get; }
|
||||
|
||||
@ -270,10 +238,7 @@ namespace osu.Game.Overlays.Dashboard
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// this is SHOCKING
|
||||
Activity = { BindTarget = Activity },
|
||||
Status = { BindTarget = Status },
|
||||
Origin = Anchor.TopCentre
|
||||
},
|
||||
new PurpleRoundedButton
|
||||
{
|
||||
|
@ -6,7 +6,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Overlays.Dashboard;
|
||||
using osu.Game.Overlays.Dashboard.Friends;
|
||||
|
||||
@ -18,6 +17,7 @@ namespace osu.Game.Overlays
|
||||
private MetadataClient metadataClient { get; set; } = null!;
|
||||
|
||||
private IBindable<bool> metadataConnected = null!;
|
||||
private IDisposable? userPresenceWatchToken;
|
||||
|
||||
public DashboardOverlay()
|
||||
: base(OverlayColourScheme.Purple)
|
||||
@ -61,9 +61,12 @@ namespace osu.Game.Overlays
|
||||
return;
|
||||
|
||||
if (State.Value == Visibility.Visible)
|
||||
metadataClient.BeginWatchingUserPresence().FireAndForget();
|
||||
userPresenceWatchToken ??= metadataClient.BeginWatchingUserPresence();
|
||||
else
|
||||
metadataClient.EndWatchingUserPresence().FireAndForget();
|
||||
{
|
||||
userPresenceWatchToken?.Dispose();
|
||||
userPresenceWatchToken = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Login
|
||||
}
|
||||
};
|
||||
|
||||
forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset");
|
||||
forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.Endpoints.WebsiteUrl}/home/password-reset");
|
||||
|
||||
password.OnCommit += (_, _) => performLogin();
|
||||
|
||||
|
@ -98,7 +98,7 @@ namespace osu.Game.Overlays.Login
|
||||
explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam);
|
||||
// We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something).
|
||||
explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the ");
|
||||
explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.WebsiteRootUrl}/home/password-reset");
|
||||
explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.Endpoints.WebsiteUrl}/home/password-reset");
|
||||
explainText.AddText(". You can also ");
|
||||
explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () =>
|
||||
{
|
||||
|
@ -124,12 +124,12 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
}
|
||||
|
||||
topLinkContainer.AddText("Contributed ");
|
||||
topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden);
|
||||
topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.Endpoints.WebsiteUrl}/users/{user.Id}/posts", creationParameters: embolden);
|
||||
|
||||
addSpacer(topLinkContainer);
|
||||
|
||||
topLinkContainer.AddText("Posted ");
|
||||
topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden);
|
||||
topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.Endpoints.WebsiteUrl}/comments?user_id={user.Id}", creationParameters: embolden);
|
||||
|
||||
string websiteWithoutProtocol = user.Website;
|
||||
|
||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
Texture = textures.Get(banner.Image),
|
||||
};
|
||||
|
||||
Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}");
|
||||
Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/tournaments/{banner.TournamentId}");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -213,7 +213,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
cover.User = user;
|
||||
avatar.User = user;
|
||||
usernameText.Text = user?.Username ?? string.Empty;
|
||||
openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}";
|
||||
openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}";
|
||||
userFlag.CountryCode = user?.CountryCode ?? default;
|
||||
userCountryText.Text = (user?.CountryCode ?? default).GetDescription();
|
||||
userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default);
|
||||
|
@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
|
||||
private void addBeatmapsetLink()
|
||||
=> content.AddLink(activity.Beatmapset.AsNonNull().Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset.AsNonNull().Url), creationParameters: t => t.Font = getLinkFont());
|
||||
|
||||
private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.WebsiteRootUrl}{url}").Argument.AsNonNull();
|
||||
private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.Endpoints.WebsiteUrl}{url}").Argument.AsNonNull();
|
||||
|
||||
private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular)
|
||||
=> OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true);
|
||||
|
@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Wiki
|
||||
Padding = new MarginPadding(padding),
|
||||
Child = new WikiPanelMarkdownContainer(isFullWidth)
|
||||
{
|
||||
CurrentPath = $@"{api.WebsiteRootUrl}/wiki/",
|
||||
CurrentPath = $@"{api.Endpoints.WebsiteUrl}/wiki/",
|
||||
Text = text,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y
|
||||
|
@ -167,7 +167,7 @@ namespace osu.Game.Overlays
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/{path.Value}/", response.Markdown));
|
||||
LoadDisplay(articlePage = new WikiArticlePage($@"{api.Endpoints.WebsiteUrl}/wiki/{path.Value}/", response.Markdown));
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,7 +176,7 @@ namespace osu.Game.Overlays
|
||||
wikiData.Value = null;
|
||||
path.Value = "error";
|
||||
|
||||
LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/",
|
||||
LoadDisplay(articlePage = new WikiArticlePage($@"{api.Endpoints.WebsiteUrl}/wiki/",
|
||||
$"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page]({INDEX_PATH})."));
|
||||
}
|
||||
|
||||
|
151
osu.Game/Screens/Edit/BookmarkController.cs
Normal file
151
osu.Game/Screens/Edit/BookmarkController.cs
Normal file
@ -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 System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit.Components.Menus;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
public partial class BookmarkController : Component, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
/// <summary>
|
||||
/// Bookmarks menu item (with submenu containing options). Should be added to the <see cref="Editor"/>'s global menu.
|
||||
/// </summary>
|
||||
public EditorMenuItem Menu { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private EditorClock clock { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
private readonly BindableList<int> bookmarks = new BindableList<int>();
|
||||
|
||||
private readonly EditorMenuItem removeBookmarkMenuItem;
|
||||
private readonly EditorMenuItem seekToPreviousBookmarkMenuItem;
|
||||
private readonly EditorMenuItem seekToNextBookmarkMenuItem;
|
||||
private readonly EditorMenuItem resetBookmarkMenuItem;
|
||||
|
||||
public BookmarkController()
|
||||
{
|
||||
Menu = new EditorMenuItem(EditorStrings.Bookmarks)
|
||||
{
|
||||
Items = new MenuItem[]
|
||||
{
|
||||
new EditorMenuItem(EditorStrings.AddBookmark, MenuItemType.Standard, addBookmarkAtCurrentTime)
|
||||
{
|
||||
Hotkey = new Hotkey(GlobalAction.EditorAddBookmark),
|
||||
},
|
||||
removeBookmarkMenuItem = new EditorMenuItem(EditorStrings.RemoveClosestBookmark, MenuItemType.Destructive, removeClosestBookmark)
|
||||
{
|
||||
Hotkey = new Hotkey(GlobalAction.EditorRemoveClosestBookmark)
|
||||
},
|
||||
seekToPreviousBookmarkMenuItem = new EditorMenuItem(EditorStrings.SeekToPreviousBookmark, MenuItemType.Standard, () => seekBookmark(-1))
|
||||
{
|
||||
Hotkey = new Hotkey(GlobalAction.EditorSeekToPreviousBookmark)
|
||||
},
|
||||
seekToNextBookmarkMenuItem = new EditorMenuItem(EditorStrings.SeekToNextBookmark, MenuItemType.Standard, () => seekBookmark(1))
|
||||
{
|
||||
Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark)
|
||||
},
|
||||
resetBookmarkMenuItem = new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => dialogOverlay?.Push(new BookmarkResetDialog(editorBeatmap)))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
bookmarks.BindTo(editorBeatmap.Bookmarks);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
bool hasAnyBookmark = bookmarks.Count > 0;
|
||||
bool hasBookmarkCloseEnoughForDeletion = bookmarks.Any(b => Math.Abs(b - clock.CurrentTimeAccurate) < 2000);
|
||||
|
||||
removeBookmarkMenuItem.Action.Disabled = !hasBookmarkCloseEnoughForDeletion;
|
||||
seekToPreviousBookmarkMenuItem.Action.Disabled = !hasAnyBookmark;
|
||||
seekToNextBookmarkMenuItem.Action.Disabled = !hasAnyBookmark;
|
||||
resetBookmarkMenuItem.Action.Disabled = !hasAnyBookmark;
|
||||
}
|
||||
|
||||
private void addBookmarkAtCurrentTime()
|
||||
{
|
||||
int bookmark = (int)clock.CurrentTimeAccurate;
|
||||
int idx = bookmarks.BinarySearch(bookmark);
|
||||
if (idx < 0)
|
||||
bookmarks.Insert(~idx, bookmark);
|
||||
}
|
||||
|
||||
private void removeClosestBookmark()
|
||||
{
|
||||
if (removeBookmarkMenuItem.Action.Disabled)
|
||||
return;
|
||||
|
||||
int closestBookmark = bookmarks.MinBy(b => Math.Abs(b - clock.CurrentTimeAccurate));
|
||||
bookmarks.Remove(closestBookmark);
|
||||
}
|
||||
|
||||
private void seekBookmark(int direction)
|
||||
{
|
||||
int? targetBookmark = direction < 1
|
||||
? bookmarks.Cast<int?>().LastOrDefault(b => b < clock.CurrentTimeAccurate)
|
||||
: bookmarks.Cast<int?>().FirstOrDefault(b => b > clock.CurrentTimeAccurate);
|
||||
|
||||
if (targetBookmark != null)
|
||||
clock.SeekSmoothlyTo(targetBookmark.Value);
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.EditorSeekToPreviousBookmark:
|
||||
seekBookmark(-1);
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorSeekToNextBookmark:
|
||||
seekBookmark(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.EditorAddBookmark:
|
||||
addBookmarkAtCurrentTime();
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorRemoveClosestBookmark:
|
||||
removeClosestBookmark();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -317,6 +317,9 @@ namespace osu.Game.Screens.Edit
|
||||
workingBeatmapUpdated = true;
|
||||
});
|
||||
|
||||
var bookmarkController = new BookmarkController();
|
||||
AddInternal(bookmarkController);
|
||||
|
||||
OsuMenuItem undoMenuItem;
|
||||
OsuMenuItem redoMenuItem;
|
||||
|
||||
@ -442,29 +445,7 @@ namespace osu.Game.Screens.Edit
|
||||
Items = new MenuItem[]
|
||||
{
|
||||
new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime),
|
||||
new EditorMenuItem(EditorStrings.Bookmarks)
|
||||
{
|
||||
Items = new MenuItem[]
|
||||
{
|
||||
new EditorMenuItem(EditorStrings.AddBookmark, MenuItemType.Standard, addBookmarkAtCurrentTime)
|
||||
{
|
||||
Hotkey = new Hotkey(GlobalAction.EditorAddBookmark),
|
||||
},
|
||||
new EditorMenuItem(EditorStrings.RemoveClosestBookmark, MenuItemType.Destructive, removeBookmarksInProximityToCurrentTime)
|
||||
{
|
||||
Hotkey = new Hotkey(GlobalAction.EditorRemoveClosestBookmark)
|
||||
},
|
||||
new EditorMenuItem(EditorStrings.SeekToPreviousBookmark, MenuItemType.Standard, () => seekBookmark(-1))
|
||||
{
|
||||
Hotkey = new Hotkey(GlobalAction.EditorSeekToPreviousBookmark)
|
||||
},
|
||||
new EditorMenuItem(EditorStrings.SeekToNextBookmark, MenuItemType.Standard, () => seekBookmark(1))
|
||||
{
|
||||
Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark)
|
||||
},
|
||||
new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => dialogOverlay?.Push(new BookmarkResetDialog(editorBeatmap)))
|
||||
}
|
||||
}
|
||||
bookmarkController.Menu,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -800,14 +781,6 @@ namespace osu.Game.Screens.Edit
|
||||
case GlobalAction.EditorSeekToNextSamplePoint:
|
||||
seekSamplePoint(1);
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorSeekToPreviousBookmark:
|
||||
seekBookmark(-1);
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorSeekToNextBookmark:
|
||||
seekBookmark(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.Repeat)
|
||||
@ -815,14 +788,6 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.EditorAddBookmark:
|
||||
addBookmarkAtCurrentTime();
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorRemoveClosestBookmark:
|
||||
removeBookmarksInProximityToCurrentTime();
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorCloneSelection:
|
||||
Clone();
|
||||
return true;
|
||||
@ -855,19 +820,6 @@ namespace osu.Game.Screens.Edit
|
||||
return false;
|
||||
}
|
||||
|
||||
private void addBookmarkAtCurrentTime()
|
||||
{
|
||||
int bookmark = (int)clock.CurrentTimeAccurate;
|
||||
int idx = editorBeatmap.Bookmarks.BinarySearch(bookmark);
|
||||
if (idx < 0)
|
||||
editorBeatmap.Bookmarks.Insert(~idx, bookmark);
|
||||
}
|
||||
|
||||
private void removeBookmarksInProximityToCurrentTime()
|
||||
{
|
||||
editorBeatmap.Bookmarks.RemoveAll(b => Math.Abs(b - clock.CurrentTimeAccurate) < 2000);
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
@ -1202,16 +1154,6 @@ namespace osu.Game.Screens.Edit
|
||||
clock.SeekSmoothlyTo(found.StartTime);
|
||||
}
|
||||
|
||||
private void seekBookmark(int direction)
|
||||
{
|
||||
int? targetBookmark = direction < 1
|
||||
? editorBeatmap.Bookmarks.Cast<int?>().LastOrDefault(b => b < clock.CurrentTimeAccurate)
|
||||
: editorBeatmap.Bookmarks.Cast<int?>().FirstOrDefault(b => b > clock.CurrentTimeAccurate);
|
||||
|
||||
if (targetBookmark != null)
|
||||
clock.SeekSmoothlyTo(targetBookmark.Value);
|
||||
}
|
||||
|
||||
private void seekSamplePoint(int direction)
|
||||
{
|
||||
double currentTime = clock.CurrentTimeAccurate;
|
||||
|
@ -115,7 +115,9 @@ namespace osu.Game.Screens.Edit
|
||||
if (editorBeatmap.Bookmarks.Contains(newBookmark))
|
||||
continue;
|
||||
|
||||
editorBeatmap.Bookmarks.Add(newBookmark);
|
||||
int idx = editorBeatmap.Bookmarks.BinarySearch(newBookmark);
|
||||
if (idx < 0)
|
||||
editorBeatmap.Bookmarks.Insert(~idx, newBookmark);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,14 +46,14 @@ namespace osu.Game.Screens.Edit.Submission
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Caption = BeatmapSubmissionStrings.MappingHelpForumDescription,
|
||||
ButtonText = BeatmapSubmissionStrings.MappingHelpForum,
|
||||
Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/56"),
|
||||
Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/forums/56"),
|
||||
},
|
||||
new FormButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Caption = BeatmapSubmissionStrings.ModdingQueuesForumDescription,
|
||||
ButtonText = BeatmapSubmissionStrings.ModdingQueuesForum,
|
||||
Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/60"),
|
||||
Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/forums/60"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
212
osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs
Normal file
212
osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs
Normal file
@ -0,0 +1,212 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
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.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Submission
|
||||
{
|
||||
public partial class SubmissionStageProgress : CompositeDrawable
|
||||
{
|
||||
public LocalisableString StageDescription { get; init; }
|
||||
|
||||
private Bindable<StageStatusType> status { get; } = new Bindable<StageStatusType>();
|
||||
|
||||
private Bindable<float?> progress { get; } = new Bindable<float?>();
|
||||
|
||||
private Container progressBarContainer = null!;
|
||||
private Box progressBar = null!;
|
||||
private Container iconContainer = null!;
|
||||
private OsuTextFlowContainer errorMessage = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Text = StageDescription,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
iconContainer = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Children =
|
||||
[
|
||||
progressBarContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Width = 150,
|
||||
Height = 10,
|
||||
CornerRadius = 5,
|
||||
Masking = true,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background6,
|
||||
},
|
||||
progressBar = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 0,
|
||||
Colour = colourProvider.Highlight1,
|
||||
}
|
||||
}
|
||||
},
|
||||
errorMessage = new OsuTextFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
// should really be `CentreRight` too, but that's broken due to a framework bug
|
||||
// (https://github.com/ppy/osu-framework/issues/5084)
|
||||
TextAnchor = Anchor.BottomRight,
|
||||
Width = 450,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
Colour = colours.Red1,
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
status.BindValueChanged(_ => Scheduler.AddOnce(updateStatus), true);
|
||||
progress.BindValueChanged(_ => Scheduler.AddOnce(updateProgress), true);
|
||||
}
|
||||
|
||||
public void SetNotStarted() => status.Value = StageStatusType.NotStarted;
|
||||
|
||||
public void SetInProgress(float? progress = null)
|
||||
{
|
||||
this.progress.Value = progress;
|
||||
status.Value = StageStatusType.InProgress;
|
||||
}
|
||||
|
||||
public void SetCompleted() => status.Value = StageStatusType.Completed;
|
||||
|
||||
public void SetFailed(string reason)
|
||||
{
|
||||
status.Value = StageStatusType.Failed;
|
||||
errorMessage.Text = reason;
|
||||
}
|
||||
|
||||
public void SetCanceled() => status.Value = StageStatusType.Canceled;
|
||||
|
||||
private const float transition_duration = 200;
|
||||
|
||||
private void updateProgress()
|
||||
{
|
||||
if (progress.Value != null)
|
||||
progressBar.ResizeWidthTo(progress.Value.Value, transition_duration, Easing.OutQuint);
|
||||
|
||||
progressBarContainer.FadeTo(status.Value == StageStatusType.InProgress && progress.Value != null ? 1 : 0, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void updateStatus()
|
||||
{
|
||||
progressBarContainer.FadeTo(status.Value == StageStatusType.InProgress && progress.Value != null ? 1 : 0, transition_duration, Easing.OutQuint);
|
||||
errorMessage.FadeTo(status.Value == StageStatusType.Failed ? 1 : 0, transition_duration, Easing.OutQuint);
|
||||
|
||||
iconContainer.Clear();
|
||||
iconContainer.ClearTransforms();
|
||||
|
||||
switch (status.Value)
|
||||
{
|
||||
case StageStatusType.InProgress:
|
||||
iconContainer.Child = new LoadingSpinner
|
||||
{
|
||||
Size = new Vector2(16),
|
||||
State = { Value = Visibility.Visible, },
|
||||
};
|
||||
iconContainer.Colour = colours.Orange1;
|
||||
break;
|
||||
|
||||
case StageStatusType.Completed:
|
||||
iconContainer.Child = new SpriteIcon
|
||||
{
|
||||
Icon = FontAwesome.Solid.CheckCircle,
|
||||
Size = new Vector2(16),
|
||||
};
|
||||
iconContainer.Colour = colours.Green1;
|
||||
iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint);
|
||||
break;
|
||||
|
||||
case StageStatusType.Failed:
|
||||
iconContainer.Child = new SpriteIcon
|
||||
{
|
||||
Icon = FontAwesome.Solid.ExclamationCircle,
|
||||
Size = new Vector2(16),
|
||||
};
|
||||
iconContainer.Colour = colours.Red1;
|
||||
iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint);
|
||||
break;
|
||||
|
||||
case StageStatusType.Canceled:
|
||||
iconContainer.Child = new SpriteIcon
|
||||
{
|
||||
Icon = FontAwesome.Solid.Ban,
|
||||
Size = new Vector2(16),
|
||||
};
|
||||
iconContainer.Colour = colours.Gray8;
|
||||
iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public enum StageStatusType
|
||||
{
|
||||
NotStarted,
|
||||
InProgress,
|
||||
Completed,
|
||||
Failed,
|
||||
Canceled,
|
||||
}
|
||||
}
|
||||
}
|
@ -361,7 +361,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
|
||||
return items.ToArray();
|
||||
|
||||
string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}";
|
||||
string formatRoomUrl(long id) => $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{id}";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -284,6 +284,13 @@ namespace osu.Game.Screens.Play
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// zero scores should also never be submitted.
|
||||
if (score.ScoreInfo.TotalScore == 0)
|
||||
{
|
||||
Logger.Log("Zero score, skipping score submission");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// mind the timing of this.
|
||||
// once `scoreSubmissionSource` is created, it is presumed that submission is taking place in the background,
|
||||
// so all exceptional circumstances that would disallow submission must be handled above.
|
||||
|
@ -11,13 +11,15 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osu.Game.Utils;
|
||||
@ -67,7 +69,7 @@ namespace osu.Game.Screens.Ranking.Contracted
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 1,
|
||||
Offset = new Vector2(0, 4)
|
||||
Offset = new Vector2(0, 2)
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -100,10 +102,10 @@ namespace osu.Game.Screens.Ranking.Contracted
|
||||
CornerRadius = 20,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Colour = Color4.Black.Opacity(0.15f),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 8,
|
||||
Offset = new Vector2(0, 4),
|
||||
Offset = new Vector2(0, 1),
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
@ -134,14 +136,33 @@ namespace osu.Game.Screens.Ranking.Contracted
|
||||
createStatistic(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, $"{score.Accuracy.FormatAccuracy()}"),
|
||||
}
|
||||
},
|
||||
new ModFlowDisplay
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Current = { Value = score.Mods },
|
||||
IconScale = 0.5f,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Full,
|
||||
Spacing = new Vector2(3),
|
||||
ChildrenEnumerable =
|
||||
[
|
||||
new DifficultyIcon(score.BeatmapInfo!, score.Ruleset)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Size = new Vector2(20),
|
||||
TooltipType = DifficultyIconTooltipType.Extended,
|
||||
Margin = new MarginPadding { Right = 2 }
|
||||
},
|
||||
..
|
||||
score.Mods.AsOrdered().Select(m => new ModIcon(m)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Scale = new Vector2(0.3f),
|
||||
Margin = new MarginPadding { Top = -6 }
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,6 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
|
||||
private readonly List<StatisticDisplay> statisticDisplays = new List<StatisticDisplay>();
|
||||
|
||||
private FillFlowContainer starAndModDisplay;
|
||||
private RollingCounter<long> scoreCounter;
|
||||
|
||||
[Resolved]
|
||||
@ -139,12 +138,35 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
},
|
||||
starAndModDisplay = new FillFlowContainer
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(5, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new StarRatingDisplay(beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely() ?? default)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft
|
||||
},
|
||||
new DifficultyIcon(beatmap, score.Ruleset)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Size = new Vector2(20),
|
||||
TooltipType = DifficultyIconTooltipType.Extended,
|
||||
},
|
||||
new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
ExpansionMode = ExpansionMode.AlwaysExpanded,
|
||||
Scale = new Vector2(0.5f),
|
||||
Current = { Value = score.Mods }
|
||||
}
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
@ -225,29 +247,6 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
|
||||
if (score.Date != default)
|
||||
AddInternal(new PlayedOnText(score.Date));
|
||||
|
||||
var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely();
|
||||
|
||||
if (starDifficulty != null)
|
||||
{
|
||||
starAndModDisplay.Add(new StarRatingDisplay(starDifficulty.Value)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft
|
||||
});
|
||||
}
|
||||
|
||||
if (score.Mods.Any())
|
||||
{
|
||||
starAndModDisplay.Add(new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
ExpansionMode = ExpansionMode.AlwaysExpanded,
|
||||
Scale = new Vector2(0.5f),
|
||||
Current = { Value = score.Mods }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -106,11 +106,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
private GroupDefinition? lastSelectedGroup;
|
||||
private BeatmapInfo? lastSelectedBeatmap;
|
||||
|
||||
protected override bool HandleItemSelected(object? model)
|
||||
protected override void HandleItemActivated(CarouselItem item)
|
||||
{
|
||||
base.HandleItemSelected(model);
|
||||
|
||||
switch (model)
|
||||
switch (item.Model)
|
||||
{
|
||||
case GroupDefinition group:
|
||||
// Special case – collapsing an open group.
|
||||
@ -118,37 +116,42 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
setExpansionStateOfGroup(lastSelectedGroup, false);
|
||||
lastSelectedGroup = null;
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedGroup(group);
|
||||
return false;
|
||||
return;
|
||||
|
||||
case BeatmapSetInfo setInfo:
|
||||
// Selecting a set isn't valid – let's re-select the first difficulty.
|
||||
CurrentSelection = setInfo.Beatmaps.First();
|
||||
return false;
|
||||
return;
|
||||
|
||||
case BeatmapInfo beatmapInfo:
|
||||
|
||||
// If we have groups, we need to account for them.
|
||||
if (Criteria.SplitOutDifficulties)
|
||||
{
|
||||
// Find the containing group. There should never be too many groups so iterating is efficient enough.
|
||||
GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key;
|
||||
|
||||
if (group != null)
|
||||
setExpandedGroup(group);
|
||||
CurrentSelection = beatmapInfo;
|
||||
return;
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
protected override void HandleItemSelected(object? model)
|
||||
{
|
||||
base.HandleItemSelected(model);
|
||||
|
||||
switch (model)
|
||||
{
|
||||
case BeatmapSetInfo:
|
||||
case GroupDefinition:
|
||||
throw new InvalidOperationException("Groups should never become selected");
|
||||
|
||||
case BeatmapInfo beatmapInfo:
|
||||
// Find any containing group. There should never be too many groups so iterating is efficient enough.
|
||||
GroupDefinition? containingGroup = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key;
|
||||
|
||||
if (containingGroup != null)
|
||||
setExpandedGroup(containingGroup);
|
||||
setExpandedSet(beatmapInfo);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool CheckValidForGroupSelection(CarouselItem item)
|
||||
@ -159,7 +162,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
return true;
|
||||
|
||||
case BeatmapInfo:
|
||||
return Criteria.SplitOutDifficulties;
|
||||
return !grouping.BeatmapSetsGroupedTogether;
|
||||
|
||||
case GroupDefinition:
|
||||
return false;
|
||||
@ -180,13 +183,47 @@ namespace osu.Game.Screens.SelectV2
|
||||
private void setExpansionStateOfGroup(GroupDefinition group, bool expanded)
|
||||
{
|
||||
if (grouping.GroupItems.TryGetValue(group, out var items))
|
||||
{
|
||||
if (expanded)
|
||||
{
|
||||
foreach (var i in items)
|
||||
{
|
||||
if (i.Model is GroupDefinition)
|
||||
i.IsExpanded = expanded;
|
||||
switch (i.Model)
|
||||
{
|
||||
case GroupDefinition:
|
||||
i.IsExpanded = true;
|
||||
break;
|
||||
|
||||
case BeatmapSetInfo set:
|
||||
// Case where there are set headers, header should be visible
|
||||
// and items should use the set's expanded state.
|
||||
i.IsVisible = true;
|
||||
setExpansionStateOfSetItems(set, i.IsExpanded);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Case where there are no set headers, all items should be visible.
|
||||
if (!grouping.BeatmapSetsGroupedTogether)
|
||||
i.IsVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
i.IsVisible = expanded;
|
||||
{
|
||||
foreach (var i in items)
|
||||
{
|
||||
switch (i.Model)
|
||||
{
|
||||
case GroupDefinition:
|
||||
i.IsExpanded = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
i.IsVisible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -263,7 +300,5 @@ namespace osu.Game.Screens.SelectV2
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record GroupDefinition(string Title);
|
||||
|
||||
public record StarsGroupDefinition(int StarNumber);
|
||||
public record GroupDefinition(object Data, string Title);
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -14,6 +13,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public class BeatmapCarouselFilterGrouping : ICarouselFilter
|
||||
{
|
||||
public bool BeatmapSetsGroupedTogether { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection.
|
||||
/// </summary>
|
||||
@ -34,102 +35,106 @@ namespace osu.Game.Screens.SelectV2
|
||||
this.getCriteria = getCriteria;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken) => await Task.Run(() =>
|
||||
public async Task<IEnumerable<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken)
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
bool groupSetsTogether;
|
||||
|
||||
setItems.Clear();
|
||||
groupItems.Clear();
|
||||
|
||||
var criteria = getCriteria();
|
||||
var newItems = new List<CarouselItem>(items.Count());
|
||||
var newItems = new List<CarouselItem>();
|
||||
|
||||
// Add criteria groups.
|
||||
switch (criteria.Group)
|
||||
{
|
||||
default:
|
||||
groupSetsTogether = true;
|
||||
newItems.AddRange(items);
|
||||
break;
|
||||
BeatmapInfo? lastBeatmap = null;
|
||||
GroupDefinition? lastGroup = null;
|
||||
|
||||
case GroupMode.Difficulty:
|
||||
groupSetsTogether = false;
|
||||
int starGroup = int.MinValue;
|
||||
HashSet<CarouselItem>? currentGroupItems = null;
|
||||
HashSet<CarouselItem>? currentSetItems = null;
|
||||
|
||||
BeatmapSetsGroupedTogether = criteria.Group != GroupMode.Difficulty;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var b = (BeatmapInfo)item.Model;
|
||||
var beatmap = (BeatmapInfo)item.Model;
|
||||
|
||||
if (b.StarRating > starGroup)
|
||||
if (createGroupIfRequired(criteria, beatmap, lastGroup) is GroupDefinition newGroup)
|
||||
{
|
||||
starGroup = (int)Math.Floor(b.StarRating);
|
||||
var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *");
|
||||
var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT };
|
||||
// When reaching a new group, ensure we reset any beatmap set tracking.
|
||||
currentSetItems = null;
|
||||
lastBeatmap = null;
|
||||
|
||||
newItems.Add(groupItem);
|
||||
groupItems[groupDefinition] = new HashSet<CarouselItem> { groupItem };
|
||||
groupItems[newGroup] = currentGroupItems = new HashSet<CarouselItem>();
|
||||
lastGroup = newGroup;
|
||||
|
||||
addItem(new CarouselItem(newGroup)
|
||||
{
|
||||
DrawHeight = GroupPanel.HEIGHT,
|
||||
DepthLayer = -2,
|
||||
});
|
||||
}
|
||||
|
||||
newItems.Add(item);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Add set headers wherever required.
|
||||
CarouselItem? lastItem = null;
|
||||
|
||||
if (groupSetsTogether)
|
||||
if (BeatmapSetsGroupedTogether)
|
||||
{
|
||||
for (int i = 0; i < newItems.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var item = newItems[i];
|
||||
|
||||
if (item.Model is BeatmapInfo beatmap)
|
||||
{
|
||||
bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID);
|
||||
bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID;
|
||||
|
||||
if (newBeatmapSet)
|
||||
{
|
||||
var setItem = new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT };
|
||||
setItems[beatmap.BeatmapSet!] = new HashSet<CarouselItem> { setItem };
|
||||
newItems.Insert(i, setItem);
|
||||
i++;
|
||||
}
|
||||
setItems[beatmap.BeatmapSet!] = currentSetItems = new HashSet<CarouselItem>();
|
||||
|
||||
setItems[beatmap.BeatmapSet!].Add(item);
|
||||
item.IsVisible = false;
|
||||
}
|
||||
|
||||
lastItem = item;
|
||||
}
|
||||
}
|
||||
|
||||
// Link group items to their headers.
|
||||
GroupDefinition? lastGroup = null;
|
||||
|
||||
foreach (var item in newItems)
|
||||
addItem(new CarouselItem(beatmap.BeatmapSet!)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (item.Model is GroupDefinition group)
|
||||
{
|
||||
lastGroup = group;
|
||||
continue;
|
||||
DrawHeight = BeatmapSetPanel.HEIGHT,
|
||||
DepthLayer = -1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (lastGroup != null)
|
||||
addItem(item);
|
||||
lastBeatmap = beatmap;
|
||||
|
||||
void addItem(CarouselItem i)
|
||||
{
|
||||
groupItems[lastGroup].Add(item);
|
||||
item.IsVisible = false;
|
||||
newItems.Add(i);
|
||||
|
||||
currentGroupItems?.Add(i);
|
||||
currentSetItems?.Add(i);
|
||||
|
||||
i.IsVisible = i.Model is GroupDefinition || (lastGroup == null && (i.Model is BeatmapSetInfo || currentSetItems == null));
|
||||
}
|
||||
}
|
||||
|
||||
return newItems;
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private GroupDefinition? createGroupIfRequired(FilterCriteria criteria, BeatmapInfo beatmap, GroupDefinition? lastGroup)
|
||||
{
|
||||
switch (criteria.Group)
|
||||
{
|
||||
case GroupMode.Artist:
|
||||
char groupChar = lastGroup?.Data as char? ?? (char)0;
|
||||
char beatmapFirstChar = char.ToUpperInvariant(beatmap.Metadata.Artist[0]);
|
||||
|
||||
if (beatmapFirstChar > groupChar)
|
||||
return new GroupDefinition(beatmapFirstChar, $"{beatmapFirstChar}");
|
||||
|
||||
break;
|
||||
|
||||
case GroupMode.Difficulty:
|
||||
int starGroup = lastGroup?.Data as int? ?? -1;
|
||||
|
||||
if (beatmap.StarRating > starGroup)
|
||||
{
|
||||
starGroup = (int)Math.Floor(beatmap.StarRating);
|
||||
return new GroupDefinition(starGroup + 1, $"{starGroup} - {starGroup + 1} *");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
switch (criteria.Sort)
|
||||
{
|
||||
case SortMode.Artist:
|
||||
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist);
|
||||
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.Metadata.Artist, bb.Metadata.Artist);
|
||||
if (comparison == 0)
|
||||
goto case SortMode.Title;
|
||||
break;
|
||||
@ -46,7 +46,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
break;
|
||||
|
||||
case SortMode.Title:
|
||||
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title);
|
||||
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.Metadata.Title, bb.Metadata.Title);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -57,6 +57,19 @@ namespace osu.Game.Screens.SelectV2
|
||||
[Resolved]
|
||||
private BeatmapCarousel? carousel { get; set; }
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
var inputRectangle = DrawRectangle;
|
||||
|
||||
// Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel.
|
||||
//
|
||||
// Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly
|
||||
// larger hit target.
|
||||
inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f });
|
||||
|
||||
return inputRectangle.Contains(ToLocalSpace(screenSpacePos));
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
@ -75,7 +88,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
InternalChild = panel = new CarouselPanelPiece(difficulty_x_offset)
|
||||
{
|
||||
Action = onAction,
|
||||
Action = () => carousel?.Activate(Item!),
|
||||
Icon = difficultyIcon = new ConstrainedIconContainer
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
@ -251,17 +264,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
panel.AccentColour = starRatingColour;
|
||||
}
|
||||
|
||||
private void onAction()
|
||||
{
|
||||
if (carousel == null)
|
||||
return;
|
||||
|
||||
if (carousel.CurrentSelection != Item!.Model)
|
||||
carousel.CurrentSelection = Item!.Model;
|
||||
else
|
||||
carousel.TryActivateSelection();
|
||||
}
|
||||
|
||||
#region ICarouselPanel
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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;
|
||||
@ -59,7 +58,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
InternalChild = panel = new CarouselPanelPiece(set_x_offset)
|
||||
{
|
||||
Action = onAction,
|
||||
Action = () => carousel?.Activate(Item!),
|
||||
Icon = chevronIcon = new Container
|
||||
{
|
||||
Size = new Vector2(22),
|
||||
@ -173,12 +172,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
difficultiesDisplay.BeatmapSet = null;
|
||||
}
|
||||
|
||||
private void onAction()
|
||||
{
|
||||
if (carousel != null)
|
||||
carousel.CurrentSelection = Item!.Model;
|
||||
}
|
||||
|
||||
#region ICarouselPanel
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
@ -190,8 +183,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public void Activated()
|
||||
{
|
||||
// sets should never be activated.
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -86,7 +86,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
InternalChild = panel = new CarouselPanelPiece(standalone_x_offset)
|
||||
{
|
||||
Action = onAction,
|
||||
Action = () => carousel?.Activate(Item!),
|
||||
Icon = difficultyIcon = new ConstrainedIconContainer
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
@ -294,17 +294,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
difficultyStarRating.Current.Value = starDifficulty;
|
||||
}
|
||||
|
||||
private void onAction()
|
||||
{
|
||||
if (carousel == null)
|
||||
return;
|
||||
|
||||
if (carousel.CurrentSelection != Item!.Model)
|
||||
carousel.CurrentSelection = Item!.Model;
|
||||
else
|
||||
carousel.TryActivateSelection();
|
||||
}
|
||||
|
||||
#region ICarouselPanel
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
|
@ -16,6 +16,7 @@ using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@ -89,26 +90,44 @@ namespace osu.Game.Screens.SelectV2
|
||||
public object? CurrentSelection
|
||||
{
|
||||
get => currentSelection.Model;
|
||||
set => setSelection(value);
|
||||
set
|
||||
{
|
||||
if (currentSelection.Model != value)
|
||||
{
|
||||
HandleItemSelected(value);
|
||||
|
||||
if (currentSelection.Model != null)
|
||||
HandleItemDeselected(currentSelection.Model);
|
||||
|
||||
currentKeyboardSelection = new Selection(value);
|
||||
currentSelection = currentKeyboardSelection;
|
||||
selectionValid.Invalidate();
|
||||
}
|
||||
else if (currentKeyboardSelection.Model != value)
|
||||
{
|
||||
// Even if the current selection matches, let's ensure the keyboard selection is reset
|
||||
// to the newly selected object. This matches user expectations (for now).
|
||||
currentKeyboardSelection = currentSelection;
|
||||
selectionValid.Invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activate the current selection, if a selection exists and matches keyboard selection.
|
||||
/// If keyboard selection does not match selection, this will transfer the selection on first invocation.
|
||||
/// Activate the specified item.
|
||||
/// </summary>
|
||||
public void TryActivateSelection()
|
||||
/// <param name="item"></param>
|
||||
public void Activate(CarouselItem item)
|
||||
{
|
||||
if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem)
|
||||
{
|
||||
CurrentSelection = currentKeyboardSelection.Model;
|
||||
return;
|
||||
}
|
||||
// Regardless of how the item handles activation, update keyboard selection to the activated panel.
|
||||
// In other words, when a panel is clicked, keyboard selection should default to matching the clicked
|
||||
// item.
|
||||
setKeyboardSelection(item.Model);
|
||||
|
||||
if (currentSelection.CarouselItem != null)
|
||||
{
|
||||
(GetMaterialisedDrawableForItem(currentSelection.CarouselItem) as ICarouselPanel)?.Activated();
|
||||
HandleItemActivated(currentSelection.CarouselItem);
|
||||
}
|
||||
(GetMaterialisedDrawableForItem(item) as ICarouselPanel)?.Activated();
|
||||
HandleItemActivated(item);
|
||||
|
||||
selectionValid.Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -176,30 +195,28 @@ namespace osu.Game.Screens.SelectV2
|
||||
protected virtual bool CheckValidForGroupSelection(CarouselItem item) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Called when an item is "selected".
|
||||
/// Called after an item becomes the <see cref="CurrentSelection"/>.
|
||||
/// Should be used to handle any group expansion, item visibility changes, etc.
|
||||
/// </summary>
|
||||
/// <returns>Whether the item should be selected.</returns>
|
||||
protected virtual bool HandleItemSelected(object? model) => true;
|
||||
protected virtual void HandleItemSelected(object? model) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when an item is "deselected".
|
||||
/// Called when the <see cref="CurrentSelection"/> changes to a new selection.
|
||||
/// Should be used to handle any group expansion, item visibility changes, etc.
|
||||
/// </summary>
|
||||
protected virtual void HandleItemDeselected(object? model)
|
||||
{
|
||||
}
|
||||
protected virtual void HandleItemDeselected(object? model) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when an item is "activated".
|
||||
/// Called when an item is activated via user input (keyboard traversal or a mouse click).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An activated item should for instance:
|
||||
/// - Open or close a folder
|
||||
/// - Start gameplay on a beatmap difficulty.
|
||||
/// An activated item should decide to perform an action, such as:
|
||||
/// - Change its expanded state (and show / hide children items).
|
||||
/// - Set the item to the <see cref="CurrentSelection"/>.
|
||||
/// - Start gameplay on a beatmap difficulty if already selected.
|
||||
/// </remarks>
|
||||
/// <param name="item">The carousel item which was activated.</param>
|
||||
protected virtual void HandleItemActivated(CarouselItem item)
|
||||
{
|
||||
}
|
||||
protected virtual void HandleItemActivated(CarouselItem item) { }
|
||||
|
||||
#endregion
|
||||
|
||||
@ -228,21 +245,16 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private async Task performFilter()
|
||||
{
|
||||
Debug.Assert(SynchronizationContext.Current != null);
|
||||
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
var cts = new CancellationTokenSource();
|
||||
|
||||
lock (this)
|
||||
{
|
||||
cancellationSource.Cancel();
|
||||
cancellationSource = cts;
|
||||
}
|
||||
var previousCancellationSource = Interlocked.Exchange(ref cancellationSource, cts);
|
||||
await previousCancellationSource.CancelAsync().ConfigureAwait(false);
|
||||
|
||||
if (DebounceDelay > 0)
|
||||
{
|
||||
log($"Filter operation queued, waiting for {DebounceDelay} ms debounce");
|
||||
await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(true);
|
||||
await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Copy must be performed on update thread for now (see ConfigureAwait above).
|
||||
@ -266,11 +278,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
log("Cancelled due to newer request arriving");
|
||||
}
|
||||
}, cts.Token).ConfigureAwait(true);
|
||||
}, cts.Token).ConfigureAwait(false);
|
||||
|
||||
if (cts.Token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
log("Items ready for display");
|
||||
carouselItems = items.ToList();
|
||||
displayedRange = null;
|
||||
@ -279,6 +293,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
HandleItemSelected(currentSelection.Model);
|
||||
|
||||
refreshAfterSelection();
|
||||
});
|
||||
|
||||
void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}");
|
||||
}
|
||||
@ -314,7 +329,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.Select:
|
||||
TryActivateSelection();
|
||||
if (currentKeyboardSelection.CarouselItem != null)
|
||||
Activate(currentKeyboardSelection.CarouselItem);
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectNext:
|
||||
@ -383,32 +399,29 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
// If the user has a different keyboard selection and requests
|
||||
// group selection, first transfer the keyboard selection to actual selection.
|
||||
if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem)
|
||||
if (currentKeyboardSelection.CarouselItem != null && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem)
|
||||
{
|
||||
TryActivateSelection();
|
||||
|
||||
// There's a chance this couldn't resolve, at which point continue with standard traversal.
|
||||
if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem)
|
||||
Activate(currentKeyboardSelection.CarouselItem);
|
||||
return;
|
||||
}
|
||||
|
||||
int originalIndex;
|
||||
int newIndex;
|
||||
|
||||
if (currentSelection.Index == null)
|
||||
if (currentKeyboardSelection.Index == null)
|
||||
{
|
||||
// If there's no current selection, start from either end of the full list.
|
||||
newIndex = originalIndex = direction > 0 ? carouselItems.Count - 1 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
newIndex = originalIndex = currentSelection.Index.Value;
|
||||
newIndex = originalIndex = currentKeyboardSelection.Index.Value;
|
||||
|
||||
// As a second special case, if we're group selecting backwards and the current selection isn't a group,
|
||||
// make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early.
|
||||
if (direction < 0)
|
||||
{
|
||||
while (!CheckValidForGroupSelection(carouselItems[newIndex]))
|
||||
while (newIndex > 0 && !CheckValidForGroupSelection(carouselItems[newIndex]))
|
||||
newIndex--;
|
||||
}
|
||||
}
|
||||
@ -422,7 +435,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
if (CheckValidForGroupSelection(newItem))
|
||||
{
|
||||
setSelection(newItem.Model);
|
||||
HandleItemActivated(newItem);
|
||||
return;
|
||||
}
|
||||
} while (newIndex != originalIndex);
|
||||
@ -437,23 +450,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
private Selection currentKeyboardSelection = new Selection();
|
||||
private Selection currentSelection = new Selection();
|
||||
|
||||
private void setSelection(object? model)
|
||||
{
|
||||
if (currentSelection.Model == model)
|
||||
return;
|
||||
|
||||
if (HandleItemSelected(model))
|
||||
{
|
||||
if (currentSelection.Model != null)
|
||||
HandleItemDeselected(currentSelection.Model);
|
||||
|
||||
currentKeyboardSelection = new Selection(model);
|
||||
currentSelection = currentKeyboardSelection;
|
||||
}
|
||||
|
||||
selectionValid.Invalidate();
|
||||
}
|
||||
|
||||
private void setKeyboardSelection(object? model)
|
||||
{
|
||||
currentKeyboardSelection = new Selection(model);
|
||||
@ -559,6 +555,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
updateDisplayedRange(range);
|
||||
}
|
||||
|
||||
double selectedYPos = currentSelection.CarouselItem?.CarouselYPosition ?? 0;
|
||||
|
||||
foreach (var panel in scroll.Panels)
|
||||
{
|
||||
var c = (ICarouselPanel)panel;
|
||||
@ -567,8 +565,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (c.Item == null)
|
||||
continue;
|
||||
|
||||
double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0;
|
||||
scroll.Panels.ChangeChildDepth(panel, (float)Math.Abs(c.DrawYPosition - selectedYPos));
|
||||
float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / DrawHeight);
|
||||
scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth);
|
||||
|
||||
if (c.DrawYPosition != c.Item.CarouselYPosition)
|
||||
c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed);
|
||||
@ -687,6 +685,15 @@ namespace osu.Game.Screens.SelectV2
|
||||
carouselPanel.Expanded.Value = false;
|
||||
}
|
||||
|
||||
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
|
||||
{
|
||||
// handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed).
|
||||
if (invalidation.HasFlag(Invalidation.DrawSize))
|
||||
selectionValid.Invalidate();
|
||||
|
||||
return base.OnInvalidate(invalidation, source);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal helper classes
|
||||
|
@ -29,6 +29,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// </summary>
|
||||
public float DrawHeight { get; set; } = DEFAULT_HEIGHT;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the display depth relative to other <see cref="CarouselItem"/>s.
|
||||
/// </summary>
|
||||
public int DepthLayer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item is visible or hidden.
|
||||
/// </summary>
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -42,7 +41,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
InternalChild = panel = new CarouselPanelPiece(0)
|
||||
{
|
||||
Action = onAction,
|
||||
Action = () => carousel?.Activate(Item!),
|
||||
Icon = chevronIcon = new SpriteIcon
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
@ -126,12 +125,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
this.FadeInFromZero(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void onAction()
|
||||
{
|
||||
if (carousel != null)
|
||||
carousel.CurrentSelection = Item!.Model;
|
||||
}
|
||||
|
||||
#region ICarouselPanel
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
@ -143,8 +136,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public void Activated()
|
||||
{
|
||||
// groups should never be activated.
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -778,7 +778,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray()));
|
||||
|
||||
if (score.OnlineID > 0)
|
||||
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.WebsiteRootUrl}/scores/{score.OnlineID}")));
|
||||
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}")));
|
||||
|
||||
if (score.Files.Count <= 0) return items.ToArray();
|
||||
|
||||
|
@ -144,16 +144,16 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
StarsGroupDefinition group = (StarsGroupDefinition)Item.Model;
|
||||
int starNumber = (int)((GroupDefinition)Item.Model).Data;
|
||||
|
||||
Color4 colour = group.StarNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(group.StarNumber);
|
||||
Color4 contentColour = group.StarNumber >= 7 ? colours.Orange1 : colourProvider.Background5;
|
||||
Color4 colour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber);
|
||||
Color4 contentColour = starNumber >= 7 ? colours.Orange1 : colourProvider.Background5;
|
||||
|
||||
panel.AccentColour = colour;
|
||||
contentBackground.Colour = colour.Darken(0.3f);
|
||||
|
||||
starRatingDisplay.Current.Value = new StarDifficulty(group.StarNumber, 0);
|
||||
starCounter.Current = group.StarNumber;
|
||||
starRatingDisplay.Current.Value = new StarDifficulty(starNumber, 0);
|
||||
starCounter.Current = starNumber;
|
||||
|
||||
chevronIcon.Colour = contentColour;
|
||||
starCounter.Colour = contentColour;
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets;
|
||||
@ -38,6 +39,9 @@ namespace osu.Game.Screens.Spectate
|
||||
[Resolved]
|
||||
private SpectatorClient spectatorClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MetadataClient metadataClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache userLookupCache { get; set; } = null!;
|
||||
|
||||
@ -50,6 +54,7 @@ namespace osu.Game.Screens.Spectate
|
||||
private readonly Dictionary<int, SpectatorGameplayState> gameplayStates = new Dictionary<int, SpectatorGameplayState>();
|
||||
|
||||
private IDisposable? realmSubscription;
|
||||
private IDisposable? userWatchToken;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SpectatorScreen"/>.
|
||||
@ -64,6 +69,8 @@ namespace osu.Game.Screens.Spectate
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
userWatchToken = metadataClient.BeginWatchingUserPresence();
|
||||
|
||||
userLookupCache.GetUsersAsync(users.ToArray()).ContinueWith(task => Schedule(() =>
|
||||
{
|
||||
var foundUsers = task.GetResultSafely();
|
||||
@ -282,6 +289,7 @@ namespace osu.Game.Screens.Spectate
|
||||
}
|
||||
|
||||
realmSubscription?.Dispose();
|
||||
userWatchToken?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,6 @@ namespace osu.Game.Tests.Visual.Metadata
|
||||
public override IBindable<bool> IsConnected => isConnected;
|
||||
private readonly BindableBool isConnected = new BindableBool(true);
|
||||
|
||||
public override IBindable<bool> IsWatchingUserPresence => isWatchingUserPresence;
|
||||
private readonly BindableBool isWatchingUserPresence = new BindableBool();
|
||||
|
||||
public override UserPresence LocalUserPresence => localUserPresence;
|
||||
private UserPresence localUserPresence;
|
||||
|
||||
@ -34,15 +31,18 @@ namespace osu.Game.Tests.Visual.Metadata
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
public override Task BeginWatchingUserPresence()
|
||||
public event Action? OnBeginWatchingUserPresence;
|
||||
public event Action? OnEndWatchingUserPresence;
|
||||
|
||||
protected override Task BeginWatchingUserPresenceInternal()
|
||||
{
|
||||
isWatchingUserPresence.Value = true;
|
||||
OnBeginWatchingUserPresence?.Invoke();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task EndWatchingUserPresence()
|
||||
protected override Task EndWatchingUserPresenceInternal()
|
||||
{
|
||||
isWatchingUserPresence.Value = false;
|
||||
OnEndWatchingUserPresence?.Invoke();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Metadata
|
||||
{
|
||||
localUserPresence = localUserPresence with { Activity = activity };
|
||||
|
||||
if (isWatchingUserPresence.Value)
|
||||
if (IsWatchingUserPresence)
|
||||
{
|
||||
if (userPresences.ContainsKey(api.LocalUser.Value.Id))
|
||||
userPresences[api.LocalUser.Value.Id] = localUserPresence;
|
||||
@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Metadata
|
||||
{
|
||||
localUserPresence = localUserPresence with { Status = status };
|
||||
|
||||
if (isWatchingUserPresence.Value)
|
||||
if (IsWatchingUserPresence)
|
||||
{
|
||||
if (userPresences.ContainsKey(api.LocalUser.Value.Id))
|
||||
userPresences[api.LocalUser.Value.Id] = localUserPresence;
|
||||
@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Metadata
|
||||
|
||||
public override Task UserPresenceUpdated(int userId, UserPresence? presence)
|
||||
{
|
||||
if (isWatchingUserPresence.Value)
|
||||
if (IsWatchingUserPresence)
|
||||
{
|
||||
if (presence?.Status != null)
|
||||
{
|
||||
|
@ -1,10 +1,8 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -15,43 +13,51 @@ using osu.Game.Users.Drawables;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Metadata;
|
||||
|
||||
namespace osu.Game.Users
|
||||
{
|
||||
public abstract partial class ExtendedUserPanel : UserPanel
|
||||
{
|
||||
public readonly Bindable<UserStatus?> Status = new Bindable<UserStatus?>();
|
||||
protected TextFlowContainer LastVisitMessage { get; private set; } = null!;
|
||||
|
||||
public readonly IBindable<UserActivity> Activity = new Bindable<UserActivity>();
|
||||
private StatusIcon statusIcon = null!;
|
||||
private StatusText statusMessage = null!;
|
||||
|
||||
protected TextFlowContainer LastVisitMessage { get; private set; }
|
||||
[Resolved]
|
||||
private MetadataClient? metadata { get; set; }
|
||||
|
||||
private StatusIcon statusIcon;
|
||||
private StatusText statusMessage;
|
||||
private UserStatus? lastStatus;
|
||||
private UserActivity? lastActivity;
|
||||
private DateTimeOffset? lastVisit;
|
||||
|
||||
protected ExtendedUserPanel(APIUser user)
|
||||
: base(user)
|
||||
{
|
||||
lastVisit = user.LastVisit;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter;
|
||||
|
||||
Status.ValueChanged += status => displayStatus(status.NewValue, Activity.Value);
|
||||
Activity.ValueChanged += activity => displayStatus(Status.Value, activity.NewValue);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Status.TriggerChange();
|
||||
updatePresence();
|
||||
|
||||
// Colour should be applied immediately on first load.
|
||||
statusIcon.FinishTransforms();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
updatePresence();
|
||||
}
|
||||
|
||||
protected Container CreateStatusIcon() => statusIcon = new StatusIcon();
|
||||
|
||||
protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren)
|
||||
@ -70,15 +76,6 @@ namespace osu.Game.Users
|
||||
text.Origin = alignment;
|
||||
text.AutoSizeAxes = Axes.Both;
|
||||
text.Alpha = 0;
|
||||
|
||||
if (User.LastVisit.HasValue)
|
||||
{
|
||||
text.AddText(@"Last seen ");
|
||||
text.AddText(new DrawableDate(User.LastVisit.Value, italic: false)
|
||||
{
|
||||
Shadow = false
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
statusContainer.Add(statusMessage = new StatusText
|
||||
@ -91,37 +88,47 @@ namespace osu.Game.Users
|
||||
return statusContainer;
|
||||
}
|
||||
|
||||
private void displayStatus(UserStatus? status, UserActivity activity = null)
|
||||
private void updatePresence()
|
||||
{
|
||||
if (status != null)
|
||||
UserPresence? presence = metadata?.GetPresence(User.OnlineID);
|
||||
UserStatus status = presence?.Status ?? UserStatus.Offline;
|
||||
UserActivity? activity = presence?.Activity;
|
||||
|
||||
if (status == lastStatus && activity == lastActivity)
|
||||
return;
|
||||
|
||||
if (status == UserStatus.Offline && lastVisit != null)
|
||||
{
|
||||
LastVisitMessage.FadeTo(status == UserStatus.Offline && User.LastVisit.HasValue ? 1 : 0);
|
||||
LastVisitMessage.FadeTo(1);
|
||||
LastVisitMessage.Clear();
|
||||
LastVisitMessage.AddText(@"Last seen ");
|
||||
LastVisitMessage.AddText(new DrawableDate(lastVisit.Value, italic: false)
|
||||
{
|
||||
Shadow = false
|
||||
});
|
||||
}
|
||||
else
|
||||
LastVisitMessage.FadeTo(0);
|
||||
|
||||
// Set status message based on activity (if we have one) and status is not offline
|
||||
if (activity != null && status != UserStatus.Offline)
|
||||
{
|
||||
statusMessage.Text = activity.GetStatus();
|
||||
statusMessage.TooltipText = activity.GetDetails();
|
||||
statusMessage.TooltipText = activity.GetDetails() ?? string.Empty;
|
||||
statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise use only status
|
||||
else
|
||||
{
|
||||
statusMessage.Text = status.GetLocalisableDescription();
|
||||
statusMessage.TooltipText = string.Empty;
|
||||
statusIcon.FadeColour(status.Value.GetAppropriateColour(Colours), 500, Easing.OutQuint);
|
||||
|
||||
return;
|
||||
statusIcon.FadeColour(status.GetAppropriateColour(Colours), 500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
// Fallback to web status if local one is null
|
||||
if (User.IsOnline)
|
||||
{
|
||||
Status.Value = UserStatus.Online;
|
||||
return;
|
||||
}
|
||||
|
||||
Status.Value = UserStatus.Offline;
|
||||
lastStatus = status;
|
||||
lastActivity = activity;
|
||||
lastVisit = status != UserStatus.Offline ? DateTimeOffset.Now : lastVisit;
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Utils
|
||||
{
|
||||
this.game = game;
|
||||
|
||||
if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteRootUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal))
|
||||
if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
sentrySession = SentrySdk.Init(options =>
|
||||
|
Loading…
x
Reference in New Issue
Block a user