mirror of
https://github.com/ppy/osu.git
synced 2026-05-25 05:39:57 +08:00
Remove all song select v1 files
This contains no code changes (that would need review), only file deletion and extraction where required in a few odd cases.
This commit is contained in:
+1
-1
@@ -19,7 +19,7 @@ using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneBeatmapMetadataDisplay : OsuTestScene
|
||||
{
|
||||
+3
-3
@@ -6,16 +6,16 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneUserTopScoreContainer : OsuTestScene
|
||||
{
|
||||
@@ -1,52 +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.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene
|
||||
{
|
||||
private Room room = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create area", () =>
|
||||
{
|
||||
Child = new MatchBeatmapDetailArea(room = new Room())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(500),
|
||||
CreateNewItem = createNewItem
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private void createNewItem()
|
||||
{
|
||||
room.Playlist = room.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
ID = room.Playlist.Count,
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
RequiredMods = new[]
|
||||
{
|
||||
new APIMod(new OsuModHardRock()),
|
||||
new APIMod(new OsuModDoubleTime()),
|
||||
new APIMod(new OsuModAutoplay())
|
||||
}
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,162 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.Select;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[System.ComponentModel.Description("PlaySongSelect beatmap details")]
|
||||
public partial class TestSceneBeatmapDetails : OsuTestScene
|
||||
{
|
||||
private BeatmapDetails details;
|
||||
|
||||
private DummyAPIAccess api => (DummyAPIAccess)API;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
Child = details = new BeatmapDetails
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(150),
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestAllMetrics()
|
||||
{
|
||||
AddStep("all metrics", () => details.BeatmapInfo = new APIBeatmap
|
||||
{
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Source = "osu!",
|
||||
Tags = "this beatmap has all the metrics",
|
||||
Ratings = Enumerable.Range(0, 11).ToArray(),
|
||||
},
|
||||
DifficultyName = "All Metrics",
|
||||
CircleSize = 7,
|
||||
DrainRate = 1,
|
||||
OverallDifficulty = 5.7f,
|
||||
ApproachRate = 3.5f,
|
||||
StarRating = 5.3f,
|
||||
FailTimes = new APIFailTimes
|
||||
{
|
||||
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
|
||||
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAllMetricsExceptSource()
|
||||
{
|
||||
AddStep("all except source", () => details.BeatmapInfo = new APIBeatmap
|
||||
{
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Tags = "this beatmap has all the metrics",
|
||||
Ratings = Enumerable.Range(0, 11).ToArray(),
|
||||
},
|
||||
DifficultyName = "All Metrics",
|
||||
CircleSize = 7,
|
||||
DrainRate = 1,
|
||||
OverallDifficulty = 5.7f,
|
||||
ApproachRate = 3.5f,
|
||||
StarRating = 5.3f,
|
||||
FailTimes = new APIFailTimes
|
||||
{
|
||||
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
|
||||
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlyRatings()
|
||||
{
|
||||
AddStep("ratings", () => details.BeatmapInfo = new APIBeatmap
|
||||
{
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Ratings = Enumerable.Range(0, 11).ToArray(),
|
||||
Source = "osu!",
|
||||
Tags = "this beatmap has ratings metrics but not retries or fails",
|
||||
},
|
||||
DifficultyName = "Only Ratings",
|
||||
CircleSize = 6,
|
||||
DrainRate = 9,
|
||||
OverallDifficulty = 6,
|
||||
ApproachRate = 6,
|
||||
StarRating = 4.8f,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlyFailsAndRetries()
|
||||
{
|
||||
AddStep("fails retries", () => details.BeatmapInfo = new APIBeatmap
|
||||
{
|
||||
DifficultyName = "Only Retries and Fails",
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Source = "osu!",
|
||||
Tags = "this beatmap has retries and fails but no ratings",
|
||||
},
|
||||
CircleSize = 3.7f,
|
||||
DrainRate = 6,
|
||||
OverallDifficulty = 6,
|
||||
ApproachRate = 7,
|
||||
StarRating = 2.91f,
|
||||
FailTimes = new APIFailTimes
|
||||
{
|
||||
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
|
||||
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoMetrics()
|
||||
{
|
||||
AddStep("no metrics", () => details.BeatmapInfo = new APIBeatmap
|
||||
{
|
||||
DifficultyName = "No Metrics",
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Source = "osu!",
|
||||
Tags = "this beatmap has no metrics",
|
||||
},
|
||||
CircleSize = 5,
|
||||
DrainRate = 5,
|
||||
OverallDifficulty = 5.5f,
|
||||
ApproachRate = 6.5f,
|
||||
StarRating = 1.97f,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNullBeatmap()
|
||||
{
|
||||
AddStep("null beatmap", () => details.BeatmapInfo = null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineMetrics()
|
||||
{
|
||||
AddStep("online ratings/retries/fails", () => details.BeatmapInfo = new APIBeatmap
|
||||
{
|
||||
OnlineID = 162,
|
||||
});
|
||||
AddStep("set online", () => api.SetState(APIState.Online));
|
||||
AddStep("set offline", () => api.SetState(APIState.Offline));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,305 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Screens.Select;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapInfoWedge : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
private TestBeatmapInfoWedge infoWedge = null!;
|
||||
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Add(infoWedge = new TestBeatmapInfoWedge
|
||||
{
|
||||
Size = new Vector2(0.5f, 245),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding { Top = 20 }
|
||||
});
|
||||
|
||||
AddStep("show", () => infoWedge.Show());
|
||||
|
||||
selectBeatmap(Beatmap.Value.Beatmap);
|
||||
|
||||
AddWaitStep("wait for select", 3);
|
||||
|
||||
AddStep("hide", () => { infoWedge.Hide(); });
|
||||
|
||||
AddWaitStep("wait for hide", 3);
|
||||
|
||||
AddStep("show", () => { infoWedge.Show(); });
|
||||
|
||||
AddSliderStep("change star difficulty", 0, 11.9, 5.55, v =>
|
||||
{
|
||||
foreach (var hasCurrentValue in infoWedge.Info.ChildrenOfType<IHasCurrentValue<StarDifficulty>>())
|
||||
hasCurrentValue.Current.Value = new StarDifficulty(v, 0);
|
||||
});
|
||||
|
||||
foreach (var rulesetInfo in rulesets.AvailableRulesets)
|
||||
{
|
||||
var instance = rulesetInfo.CreateInstance();
|
||||
var testBeatmap = CreateTestBeatmap(rulesetInfo);
|
||||
|
||||
beatmaps.Add(testBeatmap);
|
||||
|
||||
setRuleset(rulesetInfo);
|
||||
|
||||
selectBeatmap(testBeatmap);
|
||||
|
||||
testBeatmapLabels(instance);
|
||||
|
||||
switch (instance)
|
||||
{
|
||||
case OsuRuleset:
|
||||
testInfoLabels(5);
|
||||
break;
|
||||
|
||||
case TaikoRuleset:
|
||||
testInfoLabels(5);
|
||||
break;
|
||||
|
||||
case CatchRuleset:
|
||||
testInfoLabels(5);
|
||||
break;
|
||||
|
||||
case ManiaRuleset:
|
||||
testInfoLabels(4);
|
||||
break;
|
||||
|
||||
default:
|
||||
testInfoLabels(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void testBeatmapLabels(Ruleset ruleset)
|
||||
{
|
||||
AddAssert("check version", () => infoWedge.Info.VersionLabel.Current.Value == $"{ruleset.ShortName}Version");
|
||||
AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Title");
|
||||
AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist");
|
||||
AddAssert("check author", () => infoWedge.Info.MapperContainer.ChildrenOfType<OsuSpriteText>().Any(s => s.Current.Value == $"{ruleset.ShortName}Author"));
|
||||
}
|
||||
|
||||
private void testInfoLabels(int expectedCount)
|
||||
{
|
||||
AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
|
||||
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Count() == expectedCount);
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTruncation()
|
||||
{
|
||||
selectBeatmap(CreateLongMetadata());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNullBeatmap()
|
||||
{
|
||||
selectBeatmap(null);
|
||||
AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Current.Value));
|
||||
AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
|
||||
AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
|
||||
AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.ChildrenOfType<OsuSpriteText>().Any());
|
||||
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBPMUpdates()
|
||||
{
|
||||
const double bpm = 120;
|
||||
IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
|
||||
|
||||
OsuModDoubleTime doubleTime = null!;
|
||||
|
||||
selectBeatmap(beatmap);
|
||||
checkDisplayedBPM($"{bpm}");
|
||||
|
||||
AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() });
|
||||
checkDisplayedBPM($"{bpm * 1.5f}");
|
||||
|
||||
AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2);
|
||||
checkDisplayedBPM($"{bpm * 2}");
|
||||
}
|
||||
|
||||
[TestCase(120, 125, null, "120-125 (mostly 120)")]
|
||||
[TestCase(120, 120.6, null, "120-121 (mostly 120)")]
|
||||
[TestCase(120, 120.4, null, "120")]
|
||||
[TestCase(120, 120.6, "DT", "180-181 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180-181 (mostly 180)")]
|
||||
public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay)
|
||||
{
|
||||
IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
|
||||
beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm });
|
||||
beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
|
||||
|
||||
if (mod != null)
|
||||
AddStep($"select {mod}", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateModFromAcronym(mod) });
|
||||
|
||||
selectBeatmap(beatmap);
|
||||
checkDisplayedBPM(expectedDisplay);
|
||||
}
|
||||
|
||||
private void checkDisplayedBPM(string target)
|
||||
{
|
||||
AddUntilStep($"displayed bpm is {target}", () =>
|
||||
{
|
||||
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsBpm);
|
||||
return label.Statistic.Content == target;
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestLengthUpdates()
|
||||
{
|
||||
IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
double drain = beatmap.CalculateDrainLength();
|
||||
beatmap.BeatmapInfo.Length = drain;
|
||||
|
||||
OsuModDoubleTime doubleTime = null!;
|
||||
|
||||
selectBeatmap(beatmap);
|
||||
checkDisplayedLength(drain);
|
||||
|
||||
AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() });
|
||||
checkDisplayedLength(Math.Round(drain / 1.5f));
|
||||
|
||||
AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2);
|
||||
checkDisplayedLength(Math.Round(drain / 2));
|
||||
}
|
||||
|
||||
private void checkDisplayedLength(double drain)
|
||||
{
|
||||
var displayedLength = drain.ToFormattedDuration();
|
||||
|
||||
AddUntilStep($"check map drain ({displayedLength})", () =>
|
||||
{
|
||||
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>()
|
||||
.Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength));
|
||||
return label.Statistic.Content == displayedLength.ToString();
|
||||
});
|
||||
}
|
||||
|
||||
private void setRuleset(RulesetInfo rulesetInfo)
|
||||
{
|
||||
Container? containerBefore = null;
|
||||
|
||||
AddStep("set ruleset", () =>
|
||||
{
|
||||
// wedge content is only refreshed if the ruleset changes, so only wait for load in that case.
|
||||
if (!rulesetInfo.Equals(Ruleset.Value))
|
||||
containerBefore = infoWedge.DisplayedContent;
|
||||
|
||||
Ruleset.Value = rulesetInfo;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
|
||||
}
|
||||
|
||||
private void selectBeatmap(IBeatmap? b)
|
||||
{
|
||||
Container? containerBefore = null;
|
||||
|
||||
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
|
||||
{
|
||||
containerBefore = infoWedge.DisplayedContent;
|
||||
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
|
||||
}
|
||||
|
||||
public static IBeatmap CreateTestBeatmap(RulesetInfo ruleset)
|
||||
{
|
||||
List<HitObject> objects = new List<HitObject>();
|
||||
for (double i = 0; i < 50000; i += 1000)
|
||||
objects.Add(new TestHitObject { StartTime = i });
|
||||
|
||||
return new Beatmap
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Author = { Username = $"{ruleset.ShortName}Author" },
|
||||
Artist = $"{ruleset.ShortName}Artist",
|
||||
Source = $"{ruleset.ShortName}Source",
|
||||
Title = $"{ruleset.ShortName}Title"
|
||||
},
|
||||
Ruleset = ruleset,
|
||||
StarRating = 6,
|
||||
DifficultyName = $"{ruleset.ShortName}Version",
|
||||
Difficulty = new BeatmapDifficulty()
|
||||
},
|
||||
HitObjects = objects
|
||||
};
|
||||
}
|
||||
|
||||
public static IBeatmap CreateLongMetadata()
|
||||
{
|
||||
return new Beatmap
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Author = { Username = "WWWWWWWWWWWWWWW" },
|
||||
Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist",
|
||||
Source = "Verrrrry long Source",
|
||||
Title = "Verrrrry long Title"
|
||||
},
|
||||
DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version",
|
||||
Status = BeatmapOnlineStatus.Graveyard,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private partial class TestBeatmapInfoWedge : BeatmapInfoWedge
|
||||
{
|
||||
public new Container DisplayedContent => base.DisplayedContent;
|
||||
|
||||
public new WedgeInfoText Info => base.Info;
|
||||
}
|
||||
|
||||
private class TestHitObject : ConvertHitObject;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +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.ComponentModel;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Screens.Select.Options;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[Description("bottom beatmap details")]
|
||||
public partial class TestSceneBeatmapOptionsOverlay : OsuTestScene
|
||||
{
|
||||
public TestSceneBeatmapOptionsOverlay()
|
||||
{
|
||||
var overlay = new BeatmapOptionsOverlay();
|
||||
|
||||
var colours = new OsuColour();
|
||||
|
||||
overlay.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, null);
|
||||
overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, null);
|
||||
overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null);
|
||||
overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, null);
|
||||
overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, null);
|
||||
|
||||
Add(overlay);
|
||||
|
||||
AddStep(@"Toggle", overlay.ToggleVisibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +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 NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Screens.Select;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
public partial class TestSceneFilterControl : OsuManualInputManagerTestScene
|
||||
{
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = new FilterControl
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = FilterControl.HEIGHT,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,109 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Screens.Select;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
public partial class TestSceneSongSelectFooter : OsuManualInputManagerTestScene
|
||||
{
|
||||
private FooterButtonRandom randomButton;
|
||||
|
||||
private bool nextRandomCalled;
|
||||
private bool previousRandomCalled;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
nextRandomCalled = false;
|
||||
previousRandomCalled = false;
|
||||
|
||||
Footer footer;
|
||||
|
||||
Child = footer = new Footer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
|
||||
footer.AddButton(new FooterButtonMods(), null);
|
||||
footer.AddButton(randomButton = new FooterButtonRandom
|
||||
{
|
||||
NextRandom = () => nextRandomCalled = true,
|
||||
PreviousRandom = () => previousRandomCalled = true,
|
||||
}, null);
|
||||
footer.AddButton(new FooterButtonOptions(), null);
|
||||
|
||||
InputManager.MoveMouseTo(Vector2.Zero);
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestState()
|
||||
{
|
||||
AddRepeatStep("toggle options state", () => this.ChildrenOfType<FooterButton>().Last().Enabled.Toggle(), 20);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFooterRandom()
|
||||
{
|
||||
AddStep("press F2", () => InputManager.Key(Key.F2));
|
||||
AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFooterRandomViaMouse()
|
||||
{
|
||||
AddStep("click button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(randomButton);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFooterRewind()
|
||||
{
|
||||
AddStep("press Shift+F2", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.PressKey(Key.F2);
|
||||
InputManager.ReleaseKey(Key.F2);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFooterRewindViaShiftMouseLeft()
|
||||
{
|
||||
AddStep("shift + click button", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.MoveMouseTo(randomButton);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFooterRewindViaMouseRight()
|
||||
{
|
||||
AddStep("right click button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(randomButton);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,224 +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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
public partial class TestSceneTopLocalRank : OsuTestScene
|
||||
{
|
||||
private RulesetStore rulesets = null!;
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
private ScoreManager scoreManager = null!;
|
||||
private TopLocalRank topLocalRank = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, API));
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
}
|
||||
|
||||
private BeatmapInfo importedBeatmap => beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.Ruleset.ShortName == "osu");
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("Delete all scores", () => scoreManager.Delete());
|
||||
|
||||
AddStep("Create local rank", () =>
|
||||
{
|
||||
Child = topLocalRank = new TopLocalRank(importedBeatmap)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(10),
|
||||
};
|
||||
});
|
||||
|
||||
AddAssert("No rank displayed initially", () => topLocalRank.DisplayedRank == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicImportDelete()
|
||||
{
|
||||
ScoreInfo testScoreInfo = null!;
|
||||
|
||||
AddStep("Add score for current user", () =>
|
||||
{
|
||||
testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo.User = API.LocalUser.Value;
|
||||
testScoreInfo.Rank = ScoreRank.B;
|
||||
|
||||
scoreManager.Import(testScoreInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B);
|
||||
|
||||
AddStep("Delete score", () => scoreManager.Delete(testScoreInfo));
|
||||
|
||||
AddUntilStep("No rank displayed", () => topLocalRank.DisplayedRank == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRulesetChange()
|
||||
{
|
||||
AddStep("Add score for current user", () =>
|
||||
{
|
||||
var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo.User = API.LocalUser.Value;
|
||||
testScoreInfo.Rank = ScoreRank.B;
|
||||
|
||||
scoreManager.Import(testScoreInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("Wait for initial display", () => topLocalRank.DisplayedRank == ScoreRank.B);
|
||||
|
||||
AddStep("Change ruleset", () => Ruleset.Value = rulesets.GetRuleset("fruits"));
|
||||
AddUntilStep("No rank displayed", () => topLocalRank.DisplayedRank == null);
|
||||
|
||||
AddStep("Change ruleset back", () => Ruleset.Value = rulesets.GetRuleset("osu"));
|
||||
AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHigherScoreSet()
|
||||
{
|
||||
AddStep("Add score for current user", () =>
|
||||
{
|
||||
var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo.User = API.LocalUser.Value;
|
||||
testScoreInfo.Rank = ScoreRank.B;
|
||||
|
||||
scoreManager.Import(testScoreInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B);
|
||||
|
||||
AddStep("Add higher score for current user", () =>
|
||||
{
|
||||
var testScoreInfo2 = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo2.User = API.LocalUser.Value;
|
||||
testScoreInfo2.Rank = ScoreRank.X;
|
||||
testScoreInfo2.TotalScore = 1000000;
|
||||
testScoreInfo2.Statistics = testScoreInfo2.MaximumStatistics;
|
||||
|
||||
scoreManager.Import(testScoreInfo2);
|
||||
});
|
||||
|
||||
AddUntilStep("SS rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.X);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLegacyScore()
|
||||
{
|
||||
ScoreInfo testScoreInfo = null!;
|
||||
|
||||
AddStep("Add legacy score for current user", () =>
|
||||
{
|
||||
testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo.User = API.LocalUser.Value;
|
||||
testScoreInfo.Rank = ScoreRank.B;
|
||||
|
||||
scoreManager.Import(testScoreInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B);
|
||||
|
||||
AddStep("Add higher-graded score for current user", () =>
|
||||
{
|
||||
var testScoreInfo2 = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo2.User = API.LocalUser.Value;
|
||||
testScoreInfo2.Rank = ScoreRank.X;
|
||||
testScoreInfo2.Statistics = testScoreInfo2.MaximumStatistics;
|
||||
testScoreInfo2.TotalScore = testScoreInfo.TotalScore + 1;
|
||||
|
||||
scoreManager.Import(testScoreInfo2);
|
||||
});
|
||||
|
||||
AddUntilStep("SS rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.X);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGuestScore()
|
||||
{
|
||||
AddStep("Add score for guest user", () =>
|
||||
{
|
||||
var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo.User = new GuestUser();
|
||||
testScoreInfo.Rank = ScoreRank.B;
|
||||
|
||||
scoreManager.Import(testScoreInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank, () => Is.EqualTo(ScoreRank.B));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUnknownUserScore()
|
||||
{
|
||||
AddStep("Add score for unknown user", () =>
|
||||
{
|
||||
var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo.User = new APIUser { Username = "AAA", };
|
||||
testScoreInfo.Rank = ScoreRank.S;
|
||||
|
||||
scoreManager.Import(testScoreInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("S rank displayed", () => topLocalRank.DisplayedRank, () => Is.EqualTo(ScoreRank.S));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAnotherUserScore()
|
||||
{
|
||||
AddStep("Add score for not-current user", () =>
|
||||
{
|
||||
var testScoreInfo = TestResources.CreateTestScoreInfo(importedBeatmap);
|
||||
|
||||
testScoreInfo.User = new APIUser { Username = "notme", Id = 43, };
|
||||
testScoreInfo.Rank = ScoreRank.S;
|
||||
|
||||
scoreManager.Import(testScoreInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("No rank displayed", () => topLocalRank.DisplayedRank, () => Is.Null);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (rulesets.IsNotNull())
|
||||
rulesets.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,262 +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;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Online;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneUpdateBeatmapSetButton : OsuManualInputManagerTestScene
|
||||
{
|
||||
private BeatmapCarousel carousel = null!;
|
||||
|
||||
private TestScenePlaylistsBeatmapAvailabilityTracker.TestBeatmapModelDownloader beatmapDownloader = null!;
|
||||
|
||||
private BeatmapSetInfo testBeatmapSetInfo = null!;
|
||||
|
||||
[Cached(typeof(BeatmapStore))]
|
||||
private TestBeatmapStore beatmaps = new TestBeatmapStore();
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
var importer = parent.Get<BeatmapManager>();
|
||||
|
||||
dependencies.CacheAs<BeatmapModelDownloader>(beatmapDownloader = new TestScenePlaylistsBeatmapAvailabilityTracker.TestBeatmapModelDownloader(importer, API));
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
private UpdateBeatmapSetButton? getUpdateButton() => carousel.ChildrenOfType<UpdateBeatmapSetButton>().SingleOrDefault();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create carousel", () => Child = createCarousel());
|
||||
|
||||
AddUntilStep("wait for load", () => carousel.BeatmapSetsLoaded);
|
||||
|
||||
AddAssert("update button not visible", () => getUpdateButton() == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDownloadToCompletion()
|
||||
{
|
||||
ArchiveDownloadRequest<IBeatmapSetInfo>? downloadRequest = null;
|
||||
|
||||
AddStep("update online hash", () =>
|
||||
{
|
||||
testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
|
||||
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
|
||||
|
||||
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("only one set visible", () => carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().Count() == 1);
|
||||
AddUntilStep("update button visible", () => getUpdateButton() != null);
|
||||
|
||||
AddStep("click button", () => getUpdateButton()?.TriggerClick());
|
||||
|
||||
AddUntilStep("wait for download started", () =>
|
||||
{
|
||||
downloadRequest = beatmapDownloader.GetExistingDownload(testBeatmapSetInfo);
|
||||
return downloadRequest != null;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for button disabled", () => getUpdateButton()?.Enabled.Value == false);
|
||||
|
||||
AddUntilStep("progress download to completion", () =>
|
||||
{
|
||||
if (downloadRequest is TestScenePlaylistsBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
|
||||
{
|
||||
testRequest.SetProgress(testRequest.Progress + 0.1f);
|
||||
|
||||
if (testRequest.Progress >= 1)
|
||||
{
|
||||
testRequest.TriggerSuccess();
|
||||
|
||||
// usually this would be done by the import process.
|
||||
testBeatmapSetInfo.Beatmaps.First().MD5Hash = "different hash";
|
||||
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
|
||||
|
||||
// usually this would be done by a realm subscription.
|
||||
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDownloadFailed()
|
||||
{
|
||||
ArchiveDownloadRequest<IBeatmapSetInfo>? downloadRequest = null;
|
||||
|
||||
AddStep("update online hash", () =>
|
||||
{
|
||||
testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
|
||||
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
|
||||
|
||||
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("only one set visible", () => carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().Count() == 1);
|
||||
AddUntilStep("update button visible", () => getUpdateButton() != null);
|
||||
|
||||
AddStep("click button", () => getUpdateButton()?.TriggerClick());
|
||||
|
||||
AddUntilStep("wait for download started", () =>
|
||||
{
|
||||
downloadRequest = beatmapDownloader.GetExistingDownload(testBeatmapSetInfo);
|
||||
return downloadRequest != null;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for button disabled", () => getUpdateButton()?.Enabled.Value == false);
|
||||
|
||||
AddUntilStep("progress download to failure", () =>
|
||||
{
|
||||
if (downloadRequest is TestScenePlaylistsBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
|
||||
{
|
||||
testRequest.SetProgress(testRequest.Progress + 0.1f);
|
||||
|
||||
if (testRequest.Progress >= 0.5f)
|
||||
{
|
||||
testRequest.TriggerFailure(new InvalidOperationException());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for button enabled", () => getUpdateButton()?.Enabled.Value == true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUpdateLocalBeatmap()
|
||||
{
|
||||
DialogOverlay dialogOverlay = null!;
|
||||
UpdateBeatmapSetButton? updateButton = null;
|
||||
|
||||
AddStep("create carousel with dialog overlay", () =>
|
||||
{
|
||||
dialogOverlay = new DialogOverlay();
|
||||
|
||||
Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies = new (Type, object)[] { (typeof(IDialogOverlay), dialogOverlay), },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
createCarousel(),
|
||||
dialogOverlay,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("setup beatmap state", () =>
|
||||
{
|
||||
testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
|
||||
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
|
||||
testBeatmapSetInfo.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
|
||||
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for update button", () => (updateButton = getUpdateButton()) != null);
|
||||
AddStep("click button", () => updateButton.AsNonNull().TriggerClick());
|
||||
|
||||
AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is UpdateLocalConfirmationDialog);
|
||||
AddStep("click confirmation", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().First());
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("update started", () => beatmapDownloader.GetExistingDownload(testBeatmapSetInfo) != null);
|
||||
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSplitDisplay()
|
||||
{
|
||||
ArchiveDownloadRequest<IBeatmapSetInfo>? downloadRequest = null;
|
||||
|
||||
AddStep("set difficulty sort mode", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }));
|
||||
AddStep("update online hash", () =>
|
||||
{
|
||||
testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
|
||||
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
|
||||
|
||||
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("multiple \"sets\" visible", () => carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().Count(), () => Is.GreaterThan(1));
|
||||
AddUntilStep("update button visible", getUpdateButton, () => Is.Not.Null);
|
||||
|
||||
AddStep("click button", () => getUpdateButton()?.TriggerClick());
|
||||
|
||||
AddUntilStep("wait for download started", () =>
|
||||
{
|
||||
downloadRequest = beatmapDownloader.GetExistingDownload(testBeatmapSetInfo);
|
||||
return downloadRequest != null;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for button disabled", () => getUpdateButton()?.Enabled.Value == false);
|
||||
|
||||
AddUntilStep("progress download to completion", () =>
|
||||
{
|
||||
if (downloadRequest is TestScenePlaylistsBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
|
||||
{
|
||||
testRequest.SetProgress(testRequest.Progress + 0.1f);
|
||||
|
||||
if (testRequest.Progress >= 1)
|
||||
{
|
||||
testRequest.TriggerSuccess();
|
||||
|
||||
// usually this would be done by the import process.
|
||||
testBeatmapSetInfo.Beatmaps.First().MD5Hash = "different hash";
|
||||
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
|
||||
|
||||
// usually this would be done by a realm subscription.
|
||||
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private BeatmapCarousel createCarousel()
|
||||
{
|
||||
beatmaps.BeatmapSets.Clear();
|
||||
beatmaps.BeatmapSets.Add(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(5));
|
||||
|
||||
return carousel = new BeatmapCarousel(new FilterCriteria())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -25,7 +25,7 @@ using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene
|
||||
{
|
||||
@@ -1,99 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneFooterButtonMods : OsuTestScene
|
||||
{
|
||||
private readonly TestFooterButtonMods footerButtonMods;
|
||||
|
||||
public TestSceneFooterButtonMods()
|
||||
{
|
||||
Add(footerButtonMods = new TestFooterButtonMods());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIncrementMultiplier()
|
||||
{
|
||||
var hiddenMod = new Mod[] { new OsuModHidden() };
|
||||
AddStep(@"Add Hidden", () => changeMods(hiddenMod));
|
||||
AddAssert(@"Check Hidden multiplier", () => assertModsMultiplier(hiddenMod));
|
||||
|
||||
var hardRockMod = new Mod[] { new OsuModHardRock() };
|
||||
AddStep(@"Add HardRock", () => changeMods(hardRockMod));
|
||||
AddAssert(@"Check HardRock multiplier", () => assertModsMultiplier(hardRockMod));
|
||||
|
||||
var doubleTimeMod = new Mod[] { new OsuModDoubleTime() };
|
||||
AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod));
|
||||
AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod));
|
||||
|
||||
var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
|
||||
AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods));
|
||||
AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecrementMultiplier()
|
||||
{
|
||||
var easyMod = new Mod[] { new OsuModEasy() };
|
||||
AddStep(@"Add Easy", () => changeMods(easyMod));
|
||||
AddAssert(@"Check Easy multiplier", () => assertModsMultiplier(easyMod));
|
||||
|
||||
var noFailMod = new Mod[] { new OsuModNoFail() };
|
||||
AddStep(@"Add NoFail", () => changeMods(noFailMod));
|
||||
AddAssert(@"Check NoFail multiplier", () => assertModsMultiplier(noFailMod));
|
||||
|
||||
var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() };
|
||||
AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods));
|
||||
AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleDecrementMods));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClearMultiplier()
|
||||
{
|
||||
var multipleMods = new Mod[] { new OsuModDoubleTime(), new OsuModFlashlight() };
|
||||
AddStep(@"Add mods", () => changeMods(multipleMods));
|
||||
AddStep(@"Clear selected mod", () => changeMods(Array.Empty<Mod>()));
|
||||
AddAssert(@"Check empty multiplier", () => assertModsMultiplier(Array.Empty<Mod>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUnrankedBadge()
|
||||
{
|
||||
AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() }));
|
||||
AddAssert("Unranked badge shown", () => footerButtonMods.UnrankedBadge.Alpha == 1);
|
||||
AddStep(@"Clear selected mod", () => changeMods(Array.Empty<Mod>()));
|
||||
AddAssert("Unranked badge not shown", () => footerButtonMods.UnrankedBadge.Alpha == 0);
|
||||
}
|
||||
|
||||
private void changeMods(IReadOnlyList<Mod> mods)
|
||||
{
|
||||
footerButtonMods.Current.Value = mods;
|
||||
}
|
||||
|
||||
private bool assertModsMultiplier(IEnumerable<Mod> mods)
|
||||
{
|
||||
double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||
string expectedValue = multiplier == 1 ? string.Empty : ModUtils.FormatScoreMultiplier(multiplier).ToString();
|
||||
|
||||
return expectedValue == footerButtonMods.MultiplierText.Current.Value;
|
||||
}
|
||||
|
||||
private partial class TestFooterButtonMods : FooterButtonMods
|
||||
{
|
||||
public new OsuSpriteText MultiplierText => base.MultiplierText;
|
||||
public new Drawable UnrankedBadge => base.UnrankedBadge;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Framework.Audio.Track;
|
||||
using System;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
@@ -31,7 +30,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private const float shear_width = 5f;
|
||||
|
||||
private static readonly Vector2 shear = new Vector2(shear_width / Footer.HEIGHT, 0);
|
||||
public const float HEIGHT = 50;
|
||||
|
||||
private static readonly Vector2 shear = new Vector2(shear_width / HEIGHT, 0);
|
||||
|
||||
public static readonly Vector2 SIZE_EXTENDED = new Vector2(140, 50);
|
||||
public static readonly Vector2 SIZE_RETRACTED = new Vector2(100, 50);
|
||||
|
||||
@@ -22,8 +22,8 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Overlays.SkinEditor;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using Realms;
|
||||
@@ -237,6 +237,27 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
}
|
||||
}
|
||||
|
||||
public partial class SkinDeleteDialog : DeletionDialog
|
||||
{
|
||||
private readonly Skin skin;
|
||||
|
||||
public SkinDeleteDialog(Skin skin)
|
||||
{
|
||||
this.skin = skin;
|
||||
BodyText = skin.SkinInfo.Value.Name;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SkinManager manager)
|
||||
{
|
||||
DangerousAction = () =>
|
||||
{
|
||||
manager.Delete(skin.SkinInfo.Value);
|
||||
manager.CurrentSkinInfo.SetDefault();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public partial class RenameSkinPopover : OsuPopover
|
||||
{
|
||||
[Resolved]
|
||||
|
||||
@@ -1,12 +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 osu.Game.Screens.Select;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Components
|
||||
{
|
||||
public class BeatmapDetailAreaPlaylistTabItem : BeatmapDetailAreaTabItem
|
||||
{
|
||||
public override string Name => "Playlist";
|
||||
}
|
||||
}
|
||||
@@ -1,110 +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;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Screens.Select;
|
||||
using osuTK;
|
||||
using Container = osu.Framework.Graphics.Containers.Container;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Components
|
||||
{
|
||||
public partial class MatchBeatmapDetailArea : BeatmapDetailArea
|
||||
{
|
||||
public Action? CreateNewItem;
|
||||
|
||||
private readonly Room room;
|
||||
private readonly GridContainer playlistArea;
|
||||
private readonly DrawableRoomPlaylist playlist;
|
||||
|
||||
public MatchBeatmapDetailArea(Room room)
|
||||
{
|
||||
this.room = room;
|
||||
|
||||
Add(playlistArea = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Vertical = 10 },
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Bottom = 10 },
|
||||
Child = playlist = new PlaylistsRoomSettingsPlaylist
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
}
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new RoundedButton
|
||||
{
|
||||
Text = "Add new playlist entry",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One,
|
||||
Action = () => CreateNewItem?.Invoke()
|
||||
}
|
||||
},
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 50),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
playlist.Items.BindCollectionChanged((_, __) => room.Playlist = playlist.Items.ToArray());
|
||||
|
||||
room.PropertyChanged += onRoomPropertyChanged;
|
||||
updateRoomPlaylist();
|
||||
}
|
||||
|
||||
private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(Room.Playlist))
|
||||
updateRoomPlaylist();
|
||||
}
|
||||
|
||||
private void updateRoomPlaylist()
|
||||
=> playlist.Items.ReplaceRange(0, playlist.Items.Count, room.Playlist);
|
||||
|
||||
protected override void OnTabChanged(BeatmapDetailAreaTabItem tab, bool selectedMods)
|
||||
{
|
||||
base.OnTabChanged(tab, selectedMods);
|
||||
|
||||
switch (tab)
|
||||
{
|
||||
case BeatmapDetailAreaPlaylistTabItem:
|
||||
playlistArea.Show();
|
||||
break;
|
||||
|
||||
default:
|
||||
playlistArea.Hide();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override BeatmapDetailAreaTabItem[] CreateTabItems() => base.CreateTabItems().Prepend(new BeatmapDetailAreaPlaylistTabItem()).ToArray();
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
room.PropertyChanged -= onRoomPropertyChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,108 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public abstract partial class BeatmapDetailArea : Container
|
||||
{
|
||||
private const float details_padding = 10;
|
||||
|
||||
private WorkingBeatmap beatmap;
|
||||
|
||||
public virtual WorkingBeatmap Beatmap
|
||||
{
|
||||
get => beatmap;
|
||||
set
|
||||
{
|
||||
beatmap = value;
|
||||
|
||||
Details.BeatmapInfo = value?.BeatmapInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly BeatmapDetails Details;
|
||||
|
||||
protected Bindable<BeatmapDetailAreaTabItem> CurrentTab => tabControl.Current;
|
||||
|
||||
protected Bindable<bool> CurrentModsFilter => tabControl.CurrentModsFilter;
|
||||
|
||||
private readonly Container content;
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private readonly BeatmapDetailAreaTabControl tabControl;
|
||||
|
||||
protected BeatmapDetailArea()
|
||||
{
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Top = BeatmapDetailAreaTabControl.HEIGHT },
|
||||
Child = Details = new BeatmapDetails
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Alpha = 0,
|
||||
Margin = new MarginPadding { Top = details_padding },
|
||||
}
|
||||
},
|
||||
tabControl = new BeatmapDetailAreaTabControl
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TabItems = CreateTabItems(),
|
||||
OnFilter = OnTabChanged,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the currently-displayed details.
|
||||
/// </summary>
|
||||
public virtual void Refresh()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
Details.Height = Math.Min(DrawHeight - details_padding * 3 - BeatmapDetailAreaTabControl.HEIGHT, 450);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a new tab is selected.
|
||||
/// </summary>
|
||||
/// <param name="tab">The tab that was selected.</param>
|
||||
/// <param name="selectedMods">Whether the currently-selected mods should be considered.</param>
|
||||
protected virtual void OnTabChanged(BeatmapDetailAreaTabItem tab, bool selectedMods)
|
||||
{
|
||||
switch (tab)
|
||||
{
|
||||
case BeatmapDetailAreaDetailTabItem:
|
||||
Details.Show();
|
||||
break;
|
||||
|
||||
default:
|
||||
Details.Hide();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the tabs to be displayed.
|
||||
/// </summary>
|
||||
/// <returns>The tabs.</returns>
|
||||
protected virtual BeatmapDetailAreaTabItem[] CreateTabItems() => new BeatmapDetailAreaTabItem[]
|
||||
{
|
||||
new BeatmapDetailAreaDetailTabItem(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,10 +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.
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public class BeatmapDetailAreaDetailTabItem : BeatmapDetailAreaTabItem
|
||||
{
|
||||
public override string Name => "Details";
|
||||
}
|
||||
}
|
||||
@@ -1,22 +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;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public class BeatmapDetailAreaLeaderboardTabItem<TScope> : BeatmapDetailAreaTabItem
|
||||
where TScope : Enum
|
||||
{
|
||||
public override string Name => Scope.ToString();
|
||||
|
||||
public override bool FilterableByMods => true;
|
||||
|
||||
public readonly TScope Scope;
|
||||
|
||||
public BeatmapDetailAreaLeaderboardTabItem(TScope scope)
|
||||
{
|
||||
Scope = scope;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class BeatmapDetailAreaTabControl : Container
|
||||
{
|
||||
public const float HEIGHT = 24;
|
||||
|
||||
public Bindable<BeatmapDetailAreaTabItem> Current
|
||||
{
|
||||
get => tabs.Current;
|
||||
set => tabs.Current = value;
|
||||
}
|
||||
|
||||
public Bindable<bool> CurrentModsFilter
|
||||
{
|
||||
get => modsCheckbox.Current;
|
||||
set => modsCheckbox.Current = value;
|
||||
}
|
||||
|
||||
public Action<BeatmapDetailAreaTabItem, bool> OnFilter; // passed the selected tab and if mods is checked
|
||||
|
||||
public IReadOnlyList<BeatmapDetailAreaTabItem> TabItems
|
||||
{
|
||||
get => tabs.Items;
|
||||
set => tabs.Items = value;
|
||||
}
|
||||
|
||||
private readonly OsuTabControlCheckbox modsCheckbox;
|
||||
private readonly OsuTabControl<BeatmapDetailAreaTabItem> tabs;
|
||||
private readonly Container tabsContainer;
|
||||
|
||||
public BeatmapDetailAreaTabControl()
|
||||
{
|
||||
Height = HEIGHT;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 1,
|
||||
Colour = Color4.White.Opacity(0.2f),
|
||||
},
|
||||
tabsContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = tabs = new OsuTabControl<BeatmapDetailAreaTabItem>
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
IsSwitchable = true,
|
||||
},
|
||||
},
|
||||
modsCheckbox = new OsuTabControlCheckbox
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Text = @"Selected Mods",
|
||||
Alpha = 0,
|
||||
},
|
||||
};
|
||||
|
||||
tabs.Current.ValueChanged += _ => invokeOnFilter();
|
||||
modsCheckbox.Current.ValueChanged += _ => invokeOnFilter();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colour)
|
||||
{
|
||||
modsCheckbox.AccentColour = tabs.AccentColour = colour.YellowLight;
|
||||
}
|
||||
|
||||
private void invokeOnFilter()
|
||||
{
|
||||
OnFilter?.Invoke(tabs.Current.Value, modsCheckbox.Current.Value);
|
||||
|
||||
if (tabs.Current.Value.FilterableByMods)
|
||||
{
|
||||
modsCheckbox.FadeTo(1, 200, Easing.OutQuint);
|
||||
tabsContainer.Padding = new MarginPadding { Right = 100 };
|
||||
}
|
||||
else
|
||||
{
|
||||
modsCheckbox.FadeTo(0, 200, Easing.OutQuint);
|
||||
tabsContainer.Padding = new MarginPadding();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public abstract class BeatmapDetailAreaTabItem : IEquatable<BeatmapDetailAreaTabItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of this tab, to be displayed in the tab control.
|
||||
/// </summary>
|
||||
public abstract string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the contents of this tab can be filtered by the user's currently-selected mods.
|
||||
/// </summary>
|
||||
public virtual bool FilterableByMods => false;
|
||||
|
||||
public override string ToString() => Name;
|
||||
|
||||
public bool Equals(BeatmapDetailAreaTabItem other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
|
||||
return Name == other.Name;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Name != null ? Name.GetHashCode() : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,278 +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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Overlays.BeatmapSet;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Screens.Select.Details;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class BeatmapDetails : Container
|
||||
{
|
||||
private const float spacing = 10;
|
||||
private const float transition_duration = 250;
|
||||
|
||||
private readonly UserRatings ratingsDisplay;
|
||||
private readonly MetadataSection description, source, tags;
|
||||
private readonly Container failRetryContainer;
|
||||
private readonly FailRetryGraph failRetryGraph;
|
||||
private readonly LoadingLayer loading;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private SongSelect? songSelect { get; set; }
|
||||
|
||||
private IBeatmapInfo? beatmapInfo;
|
||||
|
||||
private APIFailTimes? failTimes;
|
||||
|
||||
private int[]? ratings;
|
||||
|
||||
public IBeatmapInfo? BeatmapInfo
|
||||
{
|
||||
get => beatmapInfo;
|
||||
set
|
||||
{
|
||||
if (value == beatmapInfo) return;
|
||||
|
||||
beatmapInfo = value;
|
||||
|
||||
var onlineInfo = beatmapInfo as IBeatmapOnlineInfo;
|
||||
var onlineSetInfo = beatmapInfo?.BeatmapSet as IBeatmapSetOnlineInfo;
|
||||
|
||||
failTimes = onlineInfo?.FailTimes;
|
||||
ratings = onlineSetInfo?.Ratings;
|
||||
|
||||
Scheduler.AddOnce(updateStatistics);
|
||||
}
|
||||
}
|
||||
|
||||
public BeatmapDetails()
|
||||
{
|
||||
CornerRadius = 10;
|
||||
Masking = true;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Colour4.Black.Opacity(0.3f),
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Horizontal = spacing },
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension()
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 0.5f,
|
||||
Spacing = new Vector2(spacing),
|
||||
Padding = new MarginPadding { Right = spacing / 2 },
|
||||
Children = new[]
|
||||
{
|
||||
new DetailBox().WithChild(new OnlineViewContainer(string.Empty)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 134,
|
||||
Padding = new MarginPadding { Horizontal = spacing, Top = spacing },
|
||||
Child = ratingsDisplay = new UserRatings
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250,
|
||||
Width = 0.5f,
|
||||
ScrollbarVisible = false,
|
||||
Padding = new MarginPadding { Left = spacing / 2 },
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
LayoutDuration = transition_duration,
|
||||
LayoutEasing = Easing.OutQuad,
|
||||
Children = new[]
|
||||
{
|
||||
description = new MetadataSectionDescription(query => songSelect?.Search(query)),
|
||||
source = new MetadataSectionSource(query => songSelect?.Search(query)),
|
||||
tags = new MetadataSectionMapperTags(query => songSelect?.Search(query)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
failRetryContainer = new OnlineViewContainer("Sign in to view more details")
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = BeatmapsetsStrings.ShowInfoPointsOfFailure,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),
|
||||
},
|
||||
failRetryGraph = new FailRetryGraph
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Top = 14 + spacing / 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
loading = new LoadingLayer(true)
|
||||
};
|
||||
}
|
||||
|
||||
private void updateStatistics()
|
||||
{
|
||||
description.Metadata = BeatmapInfo?.DifficultyName ?? string.Empty;
|
||||
source.Metadata = BeatmapInfo?.Metadata.Source ?? string.Empty;
|
||||
tags.Metadata = BeatmapInfo?.Metadata.Tags ?? string.Empty;
|
||||
|
||||
// failTimes may have been previously fetched
|
||||
if (ratings != null && failTimes != null)
|
||||
{
|
||||
updateMetrics();
|
||||
return;
|
||||
}
|
||||
|
||||
// for now, let's early abort if an OnlineID is not present (should have been populated at import time).
|
||||
if (BeatmapInfo == null || BeatmapInfo.OnlineID <= 0 || api.State.Value == APIState.Offline)
|
||||
{
|
||||
updateMetrics();
|
||||
return;
|
||||
}
|
||||
|
||||
var requestedBeatmap = BeatmapInfo;
|
||||
|
||||
var lookup = new GetBeatmapRequest(requestedBeatmap);
|
||||
|
||||
lookup.Success += res =>
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (beatmapInfo != requestedBeatmap)
|
||||
// the beatmap has been changed since we started the lookup.
|
||||
return;
|
||||
|
||||
ratings = res.BeatmapSet?.Ratings;
|
||||
failTimes = res.FailTimes;
|
||||
|
||||
updateMetrics();
|
||||
});
|
||||
};
|
||||
|
||||
lookup.Failure += _ =>
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (beatmapInfo != requestedBeatmap)
|
||||
// the beatmap has been changed since we started the lookup.
|
||||
return;
|
||||
|
||||
updateMetrics();
|
||||
});
|
||||
};
|
||||
|
||||
api.Queue(lookup);
|
||||
loading.Show();
|
||||
}
|
||||
|
||||
private void updateMetrics()
|
||||
{
|
||||
bool hasMetrics = (failTimes?.Retries?.Any() ?? false) || (failTimes?.Fails?.Any() ?? false);
|
||||
|
||||
if (ratings?.Any() ?? false)
|
||||
{
|
||||
ratingsDisplay.Ratings = ratings;
|
||||
ratingsDisplay.FadeIn(transition_duration);
|
||||
}
|
||||
else
|
||||
{
|
||||
// loading or just has no data server-side.
|
||||
ratingsDisplay.Ratings = new int[10];
|
||||
ratingsDisplay.FadeTo(0.25f, transition_duration);
|
||||
}
|
||||
|
||||
if (hasMetrics)
|
||||
{
|
||||
failRetryGraph.FailTimes = failTimes;
|
||||
failRetryContainer.FadeIn(transition_duration);
|
||||
}
|
||||
else
|
||||
{
|
||||
failRetryGraph.FailTimes = new APIFailTimes
|
||||
{
|
||||
Fails = new int[100],
|
||||
Retries = new int[100],
|
||||
};
|
||||
}
|
||||
|
||||
loading.Hide();
|
||||
}
|
||||
|
||||
private partial class DetailBox : Container
|
||||
{
|
||||
private readonly Container content;
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
public DetailBox()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,517 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class BeatmapInfoWedge : VisibilityContainer
|
||||
{
|
||||
public const float BORDER_THICKNESS = 2.5f;
|
||||
private const float shear_width = 36.75f;
|
||||
|
||||
private const float transition_duration = 250;
|
||||
|
||||
private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0);
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||
|
||||
protected Container DisplayedContent { get; private set; }
|
||||
|
||||
protected WedgeInfoText Info { get; private set; }
|
||||
|
||||
public BeatmapInfoWedge()
|
||||
{
|
||||
Shear = wedged_container_shear;
|
||||
Masking = true;
|
||||
BorderColour = new Color4(221, 255, 255, 255);
|
||||
BorderThickness = BORDER_THICKNESS;
|
||||
Alpha = 0;
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = new Color4(130, 204, 255, 150),
|
||||
Radius = 15,
|
||||
Roundness = 15,
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
ruleset.BindValueChanged(_ => updateDisplay());
|
||||
}
|
||||
|
||||
private const double animation_duration = 800;
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.MoveToX(0, animation_duration, Easing.OutQuint);
|
||||
this.FadeIn(transition_duration);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.MoveToX(-100, animation_duration, Easing.In);
|
||||
this.FadeOut(transition_duration * 2, Easing.In);
|
||||
}
|
||||
|
||||
private WorkingBeatmap beatmap;
|
||||
|
||||
public WorkingBeatmap Beatmap
|
||||
{
|
||||
get => beatmap;
|
||||
set
|
||||
{
|
||||
if (beatmap == value) return;
|
||||
|
||||
beatmap = value;
|
||||
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsPresent => base.IsPresent || DisplayedContent == null; // Visibility is updated in the LoadComponentAsync callback
|
||||
|
||||
private Container loadingInfo;
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
Scheduler.AddOnce(perform);
|
||||
|
||||
void perform()
|
||||
{
|
||||
void removeOldInfo()
|
||||
{
|
||||
State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible;
|
||||
|
||||
DisplayedContent?.FadeOut(transition_duration);
|
||||
DisplayedContent?.Expire();
|
||||
DisplayedContent = null;
|
||||
}
|
||||
|
||||
if (beatmap == null)
|
||||
{
|
||||
removeOldInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
LoadComponentAsync(loadingInfo = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Shear = -Shear,
|
||||
Depth = DisplayedContent?.Depth + 1 ?? 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new BeatmapInfoWedgeBackground(beatmap),
|
||||
Info = new WedgeInfoText(beatmap, ruleset.Value),
|
||||
}
|
||||
}, loaded =>
|
||||
{
|
||||
// ensure we are the most recent loaded wedge.
|
||||
if (loaded != loadingInfo) return;
|
||||
|
||||
removeOldInfo();
|
||||
Add(DisplayedContent = loaded);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public partial class WedgeInfoText : Container
|
||||
{
|
||||
public OsuSpriteText VersionLabel { get; private set; }
|
||||
public OsuSpriteText TitleLabel { get; private set; }
|
||||
public OsuSpriteText ArtistLabel { get; private set; }
|
||||
public FillFlowContainer MapperContainer { get; private set; }
|
||||
|
||||
private Container difficultyColourBar;
|
||||
private StarRatingDisplay starRatingDisplay;
|
||||
|
||||
private ILocalisedBindableString titleBinding;
|
||||
private ILocalisedBindableString artistBinding;
|
||||
private FillFlowContainer infoLabelContainer;
|
||||
private Container bpmLabelContainer;
|
||||
private Container lengthLabelContainer;
|
||||
|
||||
private readonly WorkingBeatmap working;
|
||||
private readonly RulesetInfo ruleset;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
private ModSettingChangeTracker settingChangeTracker;
|
||||
|
||||
public WedgeInfoText(WorkingBeatmap working, RulesetInfo userRuleset)
|
||||
{
|
||||
this.working = working;
|
||||
ruleset = userRuleset ?? working.BeatmapInfo.Ruleset;
|
||||
}
|
||||
|
||||
private CancellationTokenSource cancellationSource;
|
||||
private IBindable<StarDifficulty> starDifficulty;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(LocalisationManager localisation)
|
||||
{
|
||||
var beatmapInfo = working.BeatmapInfo;
|
||||
var metadata = beatmapInfo.Metadata;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
titleBinding = localisation.GetLocalisedBindableString(new RomanisableString(metadata.TitleUnicode, metadata.Title));
|
||||
artistBinding = localisation.GetLocalisedBindableString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist));
|
||||
|
||||
const float top_height = 0.7f;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
difficultyColourBar = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 20f,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = top_height,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Alpha = 0.5f,
|
||||
X = top_height,
|
||||
Width = 1 - top_height,
|
||||
}
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Name = "Topleft-aligned metadata",
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding { Top = 10, Left = 25, Right = shear_width * 2.5f },
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
VersionLabel = new TruncatingSpriteText
|
||||
{
|
||||
Text = beatmapInfo.DifficultyName,
|
||||
Font = OsuFont.GetFont(size: 24, italics: true),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Name = "Topright-aligned metadata",
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding { Top = 14, Right = shear_width / 2 },
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Shear = wedged_container_shear,
|
||||
Spacing = new Vector2(0f, 5f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
starRatingDisplay = new StarRatingDisplay(default, animated: true)
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Shear = -wedged_container_shear,
|
||||
Alpha = 0f,
|
||||
},
|
||||
new BeatmapSetOnlineStatusPill
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Shear = -wedged_container_shear,
|
||||
TextSize = 11,
|
||||
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
|
||||
Status = beatmapInfo.Status,
|
||||
Alpha = string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? 0 : 1
|
||||
}
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Name = "Centre-aligned metadata",
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Y = -7,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding { Left = 25, Right = shear_width },
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
TitleLabel = new TruncatingSpriteText
|
||||
{
|
||||
Current = { BindTarget = titleBinding },
|
||||
Font = OsuFont.GetFont(size: 28, italics: true),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
ArtistLabel = new TruncatingSpriteText
|
||||
{
|
||||
Current = { BindTarget = artistBinding },
|
||||
Font = OsuFont.GetFont(size: 17, italics: true),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
MapperContainer = new FillFlowContainer
|
||||
{
|
||||
Margin = new MarginPadding { Top = 10 },
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = getMapper(metadata),
|
||||
},
|
||||
infoLabelContainer = new FillFlowContainer
|
||||
{
|
||||
Margin = new MarginPadding { Top = 8 },
|
||||
Spacing = new Vector2(20, 0),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addInfoLabels();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
starRatingDisplay.DisplayedStars.BindValueChanged(s =>
|
||||
{
|
||||
difficultyColourBar.Colour = colours.ForStarDifficulty(s.NewValue);
|
||||
}, true);
|
||||
|
||||
starDifficulty = difficultyCache.GetBindableDifficulty(working.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token);
|
||||
starDifficulty.BindValueChanged(s =>
|
||||
{
|
||||
starRatingDisplay.Current.Value = s.NewValue;
|
||||
|
||||
// Don't roll the counter on initial display (but still allow it to roll on applying mods etc.)
|
||||
if (!starRatingDisplay.IsPresent)
|
||||
starRatingDisplay.FinishTransforms(true);
|
||||
|
||||
starRatingDisplay.FadeIn(transition_duration);
|
||||
});
|
||||
|
||||
mods.BindValueChanged(m =>
|
||||
{
|
||||
settingChangeTracker?.Dispose();
|
||||
|
||||
refreshBPMAndLengthLabel();
|
||||
|
||||
settingChangeTracker = new ModSettingChangeTracker(m.NewValue);
|
||||
settingChangeTracker.SettingChanged += _ => refreshBPMAndLengthLabel();
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void addInfoLabels()
|
||||
{
|
||||
if (working.Beatmap?.HitObjects.Any() != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
IBeatmap playableBeatmap;
|
||||
|
||||
try
|
||||
{
|
||||
// Try to get the beatmap with the user's ruleset
|
||||
playableBeatmap = working.GetPlayableBeatmap(ruleset, Array.Empty<Mod>());
|
||||
}
|
||||
catch (BeatmapInvalidForRulesetException)
|
||||
{
|
||||
// Can't be converted to the user's ruleset, so use the beatmap's own ruleset
|
||||
playableBeatmap = working.GetPlayableBeatmap(working.BeatmapInfo.Ruleset, Array.Empty<Mod>());
|
||||
}
|
||||
|
||||
infoLabelContainer.Children = new Drawable[]
|
||||
{
|
||||
lengthLabelContainer = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
},
|
||||
bpmLabelContainer = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(20, 0),
|
||||
Children = playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray()
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Could not load beatmap successfully!");
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshBPMAndLengthLabel()
|
||||
{
|
||||
var beatmap = working.Beatmap;
|
||||
|
||||
if (beatmap == null || bpmLabelContainer == null)
|
||||
return;
|
||||
|
||||
double rate = ModUtils.CalculateRateWithMods(mods.Value);
|
||||
|
||||
int bpmMax = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMaximum, rate);
|
||||
int bpmMin = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMinimum, rate);
|
||||
int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.GetMostCommonBeatLength(), rate);
|
||||
|
||||
string labelText = bpmMin == bpmMax
|
||||
? $"{bpmMin}"
|
||||
: $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})";
|
||||
|
||||
bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic
|
||||
{
|
||||
Name = BeatmapsetsStrings.ShowStatsBpm,
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
|
||||
Content = labelText
|
||||
});
|
||||
|
||||
double drainLength = Math.Round(beatmap.CalculateDrainLength() / rate);
|
||||
double hitLength = Math.Round(beatmap.BeatmapInfo.Length / rate);
|
||||
|
||||
lengthLabelContainer.Child = new InfoLabel(new BeatmapStatistic
|
||||
{
|
||||
Name = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration()),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
|
||||
Content = hitLength.ToFormattedDuration().ToString(),
|
||||
});
|
||||
}
|
||||
|
||||
private Drawable getMapper(BeatmapMetadata metadata)
|
||||
{
|
||||
if (string.IsNullOrEmpty(metadata.Author.Username))
|
||||
return Empty();
|
||||
|
||||
return new LinkFlowContainer(s =>
|
||||
{
|
||||
s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 15);
|
||||
}).With(d =>
|
||||
{
|
||||
d.AutoSizeAxes = Axes.Both;
|
||||
d.AddText("mapped by ");
|
||||
d.AddUserLink(metadata.Author);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
settingChangeTracker?.Dispose();
|
||||
cancellationSource?.Cancel();
|
||||
}
|
||||
|
||||
public partial class InfoLabel : Container, IHasTooltip
|
||||
{
|
||||
public LocalisableString TooltipText { get; }
|
||||
|
||||
internal BeatmapStatistic Statistic { get; }
|
||||
|
||||
public InfoLabel(BeatmapStatistic statistic)
|
||||
{
|
||||
Statistic = statistic;
|
||||
TooltipText = statistic.Name;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Size = new Vector2(20),
|
||||
Children = new[]
|
||||
{
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex(@"441288"),
|
||||
Icon = FontAwesome.Solid.Square,
|
||||
Rotation = 45,
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex(@"f7dd55"),
|
||||
Icon = FontAwesome.Regular.Circle,
|
||||
Size = new Vector2(0.7f)
|
||||
},
|
||||
statistic.CreateIcon().With(i =>
|
||||
{
|
||||
i.Anchor = Anchor.Centre;
|
||||
i.Origin = Anchor.Centre;
|
||||
i.RelativeSizeAxes = Axes.Both;
|
||||
i.Colour = Color4Extensions.FromHex(@"f7dd55");
|
||||
i.Size = new Vector2(0.6f);
|
||||
}),
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Colour = new Color4(255, 221, 85, 255),
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17),
|
||||
Margin = new MarginPadding { Left = 30 },
|
||||
Text = statistic.Content,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +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 osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
internal partial class BeatmapInfoWedgeBackground : CompositeDrawable
|
||||
{
|
||||
private readonly IWorkingBeatmap beatmap;
|
||||
|
||||
public BeatmapInfoWedgeBackground(IWorkingBeatmap beatmap)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new BufferedContainer(cachedFrameBuffer: true)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
// We will create the white-to-black gradient by modulating transparency and having
|
||||
// a black backdrop. This results in an sRGB-space gradient and not linear space,
|
||||
// transitioning from white to black more perceptually uniformly.
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
},
|
||||
// We use a container, such that we can set the colour gradient to go across the
|
||||
// vertices of the masked container instead of the vertices of the (larger) sprite.
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0.3f)),
|
||||
Children = new[]
|
||||
{
|
||||
// Zoomed-in and cropped beatmap background
|
||||
new BeatmapBackgroundSprite(beatmap)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FillMode = FillMode.Fill,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,161 +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;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public class CarouselBeatmap : CarouselItem
|
||||
{
|
||||
public override float TotalHeight => DrawableCarouselBeatmap.HEIGHT;
|
||||
|
||||
public readonly BeatmapInfo BeatmapInfo;
|
||||
|
||||
public CarouselBeatmap(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
BeatmapInfo = beatmapInfo;
|
||||
State.Value = CarouselItemState.Collapsed;
|
||||
}
|
||||
|
||||
public override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmap(this);
|
||||
|
||||
public override void Filter(FilterCriteria criteria)
|
||||
{
|
||||
base.Filter(criteria);
|
||||
|
||||
Filtered.Value = !checkMatch(criteria);
|
||||
}
|
||||
|
||||
private bool checkMatch(FilterCriteria criteria)
|
||||
{
|
||||
bool match =
|
||||
criteria.Ruleset == null ||
|
||||
BeatmapInfo.Ruleset.ShortName == criteria.Ruleset.ShortName ||
|
||||
(BeatmapInfo.Ruleset.OnlineID == 0 && criteria.Ruleset.OnlineID != 0 && criteria.AllowConvertedBeatmaps);
|
||||
|
||||
if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true)
|
||||
{
|
||||
// only check ruleset equality or convertability for selected beatmap
|
||||
return match;
|
||||
}
|
||||
|
||||
if (!match) return false;
|
||||
|
||||
if (criteria.SearchTerms.Length > 0)
|
||||
{
|
||||
match = BeatmapInfo.Match(criteria.SearchTerms);
|
||||
|
||||
// if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs.
|
||||
// this should be done after text matching so we can prioritise matching numbers in metadata.
|
||||
if (!match && criteria.SearchNumber.HasValue)
|
||||
{
|
||||
match = (BeatmapInfo.OnlineID == criteria.SearchNumber.Value) ||
|
||||
(BeatmapInfo.BeatmapSet?.OnlineID == criteria.SearchNumber.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!match) return false;
|
||||
|
||||
match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating.FloorToDecimalDigits(2));
|
||||
match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.Difficulty.ApproachRate);
|
||||
match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.Difficulty.DrainRate);
|
||||
match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.Difficulty.CircleSize);
|
||||
match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.Difficulty.OverallDifficulty);
|
||||
match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length);
|
||||
match &= !criteria.LastPlayed.HasFilter || criteria.LastPlayed.IsInRange(BeatmapInfo.LastPlayed ?? DateTimeOffset.MinValue);
|
||||
match &= !criteria.DateRanked.HasFilter || (BeatmapInfo.BeatmapSet?.DateRanked != null && criteria.DateRanked.IsInRange(BeatmapInfo.BeatmapSet.DateRanked.Value));
|
||||
match &= !criteria.DateSubmitted.HasFilter || (BeatmapInfo.BeatmapSet?.DateSubmitted != null && criteria.DateSubmitted.IsInRange(BeatmapInfo.BeatmapSet.DateSubmitted.Value));
|
||||
match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM);
|
||||
|
||||
match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor);
|
||||
match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(BeatmapInfo.Status);
|
||||
|
||||
if (!match) return false;
|
||||
|
||||
match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.Author.Username);
|
||||
|
||||
if (criteria.Artist.HasFilter)
|
||||
{
|
||||
if (criteria.Artist.ExcludeTerm)
|
||||
match &= criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) && criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
|
||||
else
|
||||
match &= criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
|
||||
}
|
||||
|
||||
if (criteria.Title.HasFilter)
|
||||
{
|
||||
if (criteria.Title.ExcludeTerm)
|
||||
match &= criteria.Title.Matches(BeatmapInfo.Metadata.Title) && criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode);
|
||||
else
|
||||
match &= criteria.Title.Matches(BeatmapInfo.Metadata.Title) || criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode);
|
||||
}
|
||||
|
||||
match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(BeatmapInfo.DifficultyName);
|
||||
match &= !criteria.Source.HasFilter || criteria.Source.Matches(BeatmapInfo.Metadata.Source);
|
||||
|
||||
if (criteria.UserTags.Any())
|
||||
{
|
||||
foreach (var tagFilter in criteria.UserTags)
|
||||
{
|
||||
if (tagFilter.ExcludeTerm)
|
||||
{
|
||||
// if `ExcludeTerm` is true, `Matches()` will return true if a user tag *doesn't match* the excluded term.
|
||||
// thus, every user tag must pass this filter.
|
||||
foreach (string tag in BeatmapInfo.Metadata.UserTags)
|
||||
match &= tagFilter.Matches(tag);
|
||||
}
|
||||
else
|
||||
{
|
||||
// if `ExcludeTerm` is false, `Matches()` will return true if a user tag *matches* the expected term.
|
||||
// the expected behaviour is that a beatmap should be displayed if at least one of the user tags passes the filter.
|
||||
bool anyTagMatched = false;
|
||||
|
||||
foreach (string tag in BeatmapInfo.Metadata.UserTags)
|
||||
anyTagMatched |= tagFilter.Matches(tag);
|
||||
|
||||
match &= anyTagMatched;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);
|
||||
|
||||
if (!match) return false;
|
||||
|
||||
match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true;
|
||||
if (match && criteria.RulesetCriteria != null)
|
||||
match &= criteria.RulesetCriteria.Matches(BeatmapInfo, criteria);
|
||||
|
||||
if (match && criteria.HasOnlineID == true)
|
||||
match &= BeatmapInfo.OnlineID >= 0;
|
||||
|
||||
if (match && criteria.BeatmapSetId != null)
|
||||
match &= criteria.BeatmapSetId == BeatmapInfo.BeatmapSet?.OnlineID;
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
public override int CompareTo(FilterCriteria criteria, CarouselItem other)
|
||||
{
|
||||
if (!(other is CarouselBeatmap otherBeatmap))
|
||||
return base.CompareTo(criteria, other);
|
||||
|
||||
switch (criteria.Sort)
|
||||
{
|
||||
default:
|
||||
case SortMode.Difficulty:
|
||||
int ruleset = BeatmapInfo.Ruleset.CompareTo(otherBeatmap.BeatmapInfo.Ruleset);
|
||||
|
||||
if (ruleset != 0) return ruleset;
|
||||
|
||||
return BeatmapInfo.StarRating.CompareTo(otherBeatmap.BeatmapInfo.StarRating);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => BeatmapInfo.ToString();
|
||||
}
|
||||
}
|
||||
@@ -1,184 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public class CarouselBeatmapSet : CarouselGroupEagerSelect
|
||||
{
|
||||
public override float TotalHeight
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (State.Value)
|
||||
{
|
||||
case CarouselItemState.Selected:
|
||||
return DrawableCarouselBeatmapSet.HEIGHT + Items.Count(c => c.Visible) * DrawableCarouselBeatmap.HEIGHT;
|
||||
|
||||
default:
|
||||
return DrawableCarouselBeatmapSet.HEIGHT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<CarouselBeatmap> Beatmaps => Items.OfType<CarouselBeatmap>();
|
||||
|
||||
public BeatmapSetInfo BeatmapSet;
|
||||
|
||||
public Func<IEnumerable<BeatmapInfo>, BeatmapInfo?>? GetRecommendedBeatmap;
|
||||
|
||||
public CarouselBeatmapSet(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
BeatmapSet = beatmapSet ?? throw new ArgumentNullException(nameof(beatmapSet));
|
||||
|
||||
beatmapSet.Beatmaps
|
||||
.Where(b => !b.Hidden)
|
||||
.OrderBy(b => b.Ruleset)
|
||||
.ThenBy(b => b.StarRating)
|
||||
.Select(b => new CarouselBeatmap(b))
|
||||
.ForEach(AddItem);
|
||||
}
|
||||
|
||||
public override CarouselItem? GetNextToSelect()
|
||||
{
|
||||
if (LastSelected == null || LastSelected.Filtered.Value)
|
||||
{
|
||||
if (GetRecommendedBeatmap?.Invoke(Items.OfType<CarouselBeatmap>().Where(b => !b.Filtered.Value).Select(b => b.BeatmapInfo)) is BeatmapInfo recommended)
|
||||
return Items.OfType<CarouselBeatmap>().First(b => b.BeatmapInfo.Equals(recommended));
|
||||
}
|
||||
|
||||
return base.GetNextToSelect();
|
||||
}
|
||||
|
||||
public override int CompareTo(FilterCriteria criteria, CarouselItem other)
|
||||
{
|
||||
if (!(other is CarouselBeatmapSet otherSet))
|
||||
return base.CompareTo(criteria, other);
|
||||
|
||||
int comparison;
|
||||
|
||||
switch (criteria.Sort)
|
||||
{
|
||||
default:
|
||||
case SortMode.Artist:
|
||||
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist);
|
||||
if (comparison == 0)
|
||||
goto case SortMode.Title;
|
||||
break;
|
||||
|
||||
case SortMode.Title:
|
||||
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title);
|
||||
break;
|
||||
|
||||
case SortMode.Author:
|
||||
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username);
|
||||
break;
|
||||
|
||||
case SortMode.Source:
|
||||
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source);
|
||||
break;
|
||||
|
||||
case SortMode.DateAdded:
|
||||
comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
|
||||
break;
|
||||
|
||||
case SortMode.DateRanked:
|
||||
comparison = Nullable.Compare(otherSet.BeatmapSet.DateRanked, BeatmapSet.DateRanked);
|
||||
break;
|
||||
|
||||
case SortMode.LastPlayed:
|
||||
comparison = -compareUsingAggregateMax(otherSet, static b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds());
|
||||
break;
|
||||
|
||||
case SortMode.BPM:
|
||||
comparison = compareUsingAggregateMax(otherSet, static b => b.BPM);
|
||||
break;
|
||||
|
||||
case SortMode.Length:
|
||||
comparison = compareUsingAggregateMax(otherSet, static b => b.Length);
|
||||
break;
|
||||
|
||||
case SortMode.Difficulty:
|
||||
comparison = compareUsingAggregateMax(otherSet, static b => b.StarRating);
|
||||
break;
|
||||
|
||||
case SortMode.DateSubmitted:
|
||||
comparison = Nullable.Compare(otherSet.BeatmapSet.DateSubmitted, BeatmapSet.DateSubmitted);
|
||||
break;
|
||||
}
|
||||
|
||||
if (comparison != 0) return comparison;
|
||||
|
||||
// If the initial sort could not differentiate, attempt to use DateAdded to order sets in a stable fashion.
|
||||
// The directionality of this matches the current SortMode.DateAdded, but we may want to reconsider if that becomes a user decision (ie. asc / desc).
|
||||
comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
|
||||
|
||||
if (comparison != 0) return comparison;
|
||||
|
||||
// If DateAdded fails to break the tie, fallback to our internal GUID for stability.
|
||||
// This basically means it's a stable random sort.
|
||||
return otherSet.BeatmapSet.ID.CompareTo(BeatmapSet.ID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All beatmaps which are not filtered and valid for display.
|
||||
/// </summary>
|
||||
protected IEnumerable<BeatmapInfo> ValidBeatmaps
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var item in Items) // iterating over Items directly to not allocate 2 enumerators
|
||||
{
|
||||
if (item is CarouselBeatmap b && (!b.Filtered.Value || b.State.Value == CarouselItemState.Selected))
|
||||
yield return b.BeatmapInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether there are available beatmaps which are not filtered and valid for display.
|
||||
/// Cheaper alternative to <see cref="ValidBeatmaps"/>.Any()
|
||||
/// </summary>
|
||||
public bool HasValidBeatmaps
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var item in Items) // iterating over Items directly to not allocate 2 enumerators
|
||||
{
|
||||
if (item is CarouselBeatmap b && (!b.Filtered.Value || b.State.Value == CarouselItemState.Selected))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private int compareUsingAggregateMax(CarouselBeatmapSet other, Func<BeatmapInfo, double> func)
|
||||
{
|
||||
bool ourBeatmaps = HasValidBeatmaps;
|
||||
bool otherBeatmaps = other.HasValidBeatmaps;
|
||||
|
||||
if (!ourBeatmaps && !otherBeatmaps) return 0;
|
||||
if (!ourBeatmaps) return -1;
|
||||
if (!otherBeatmaps) return 1;
|
||||
|
||||
return ValidBeatmaps.Max(func).CompareTo(other.ValidBeatmaps.Max(func));
|
||||
}
|
||||
|
||||
public override void Filter(FilterCriteria criteria)
|
||||
{
|
||||
base.Filter(criteria);
|
||||
|
||||
Filtered.Value = Items.All(i => i.Filtered.Value);
|
||||
}
|
||||
|
||||
public override string ToString() => BeatmapSet.ToString();
|
||||
}
|
||||
}
|
||||
@@ -1,140 +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 osu.Framework.Extensions.ListExtensions;
|
||||
using osu.Framework.Lists;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
/// <summary>
|
||||
/// A group which ensures only one item is selected.
|
||||
/// </summary>
|
||||
public abstract class CarouselGroup : CarouselItem
|
||||
{
|
||||
protected CarouselGroup(List<CarouselItem>? items = null)
|
||||
{
|
||||
if (items != null) this.items = items;
|
||||
|
||||
State.ValueChanged += state =>
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case CarouselItemState.Collapsed:
|
||||
case CarouselItemState.NotSelected:
|
||||
this.items.ForEach(c => c.State.Value = CarouselItemState.Collapsed);
|
||||
break;
|
||||
|
||||
case CarouselItemState.Selected:
|
||||
this.items.ForEach(c =>
|
||||
{
|
||||
if (c.State.Value == CarouselItemState.Collapsed) c.State.Value = CarouselItemState.NotSelected;
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public override DrawableCarouselItem? CreateDrawableRepresentation() => null;
|
||||
|
||||
public SlimReadOnlyListWrapper<CarouselItem> Items => items.AsSlimReadOnly();
|
||||
|
||||
public int TotalItemsNotFiltered { get; private set; }
|
||||
|
||||
private readonly List<CarouselItem> items = new List<CarouselItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Used to assign a monotonically increasing ID to items as they are added. This member is
|
||||
/// incremented whenever an item is added.
|
||||
/// </summary>
|
||||
private ulong currentItemID;
|
||||
|
||||
private Comparer<CarouselItem>? criteriaComparer;
|
||||
private FilterCriteria? lastCriteria;
|
||||
|
||||
protected int GetIndexOfItem(CarouselItem lastSelected) => items.IndexOf(lastSelected);
|
||||
|
||||
public virtual void RemoveItem(CarouselItem i)
|
||||
{
|
||||
items.Remove(i);
|
||||
|
||||
if (!i.Filtered.Value)
|
||||
TotalItemsNotFiltered--;
|
||||
|
||||
// it's important we do the deselection after removing, so any further actions based on
|
||||
// State.ValueChanged make decisions post-removal.
|
||||
i.State.Value = CarouselItemState.Collapsed;
|
||||
}
|
||||
|
||||
public virtual void AddItem(CarouselItem i)
|
||||
{
|
||||
i.State.ValueChanged += state => ChildItemStateChanged(i, state.NewValue);
|
||||
i.ItemID = ++currentItemID;
|
||||
|
||||
if (lastCriteria != null)
|
||||
{
|
||||
i.Filter(lastCriteria);
|
||||
|
||||
int index = items.BinarySearch(i, criteriaComparer);
|
||||
if (index < 0) index = ~index; // BinarySearch hacks multiple return values with 2's complement.
|
||||
|
||||
items.Insert(index, i);
|
||||
}
|
||||
else
|
||||
{
|
||||
// criteria may be null for initial population. the filtering will be applied post-add.
|
||||
items.Add(i);
|
||||
}
|
||||
|
||||
if (!i.Filtered.Value)
|
||||
TotalItemsNotFiltered++;
|
||||
}
|
||||
|
||||
public override void Filter(FilterCriteria criteria)
|
||||
{
|
||||
base.Filter(criteria);
|
||||
|
||||
TotalItemsNotFiltered = 0;
|
||||
|
||||
foreach (var c in items)
|
||||
{
|
||||
c.Filter(criteria);
|
||||
if (!c.Filtered.Value)
|
||||
TotalItemsNotFiltered++;
|
||||
}
|
||||
|
||||
// Sorting is expensive, so only perform if it's actually changed.
|
||||
if (lastCriteria?.RequiresSorting(criteria) != false)
|
||||
{
|
||||
criteriaComparer = Comparer<CarouselItem>.Create((x, y) =>
|
||||
{
|
||||
int comparison = x.CompareTo(criteria, y);
|
||||
if (comparison != 0)
|
||||
return comparison;
|
||||
|
||||
return x.ItemID.CompareTo(y.ItemID);
|
||||
});
|
||||
|
||||
items.Sort(criteriaComparer);
|
||||
}
|
||||
|
||||
lastCriteria = criteria;
|
||||
}
|
||||
|
||||
protected virtual void ChildItemStateChanged(CarouselItem item, CarouselItemState value)
|
||||
{
|
||||
// ensure we are the only item selected
|
||||
if (value == CarouselItemState.Selected)
|
||||
{
|
||||
foreach (var b in items)
|
||||
{
|
||||
if (item == b) continue;
|
||||
|
||||
b.State.Value = CarouselItemState.NotSelected;
|
||||
}
|
||||
|
||||
State.Value = CarouselItemState.Selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,159 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
/// <summary>
|
||||
/// A group which ensures at least one item is selected (if the group itself is selected).
|
||||
/// </summary>
|
||||
public abstract class CarouselGroupEagerSelect : CarouselGroup
|
||||
{
|
||||
protected CarouselGroupEagerSelect()
|
||||
{
|
||||
State.ValueChanged += state =>
|
||||
{
|
||||
if (state.NewValue == CarouselItemState.Selected)
|
||||
attemptSelection();
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The last selected item.
|
||||
/// </summary>
|
||||
protected CarouselItem? LastSelected { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// We need to keep track of the index for cases where the selection is removed but we want to select a new item based on its old location.
|
||||
/// </summary>
|
||||
private int lastSelectedIndex;
|
||||
|
||||
/// <summary>
|
||||
/// To avoid overhead during filter operations, we don't attempt any selections until after all
|
||||
/// items have been filtered. This bool will be true during the base <see cref="Filter(FilterCriteria)"/>
|
||||
/// operation.
|
||||
/// </summary>
|
||||
protected bool DisableSelection;
|
||||
|
||||
public override void Filter(FilterCriteria criteria)
|
||||
{
|
||||
DisableSelection = true;
|
||||
base.Filter(criteria);
|
||||
DisableSelection = false;
|
||||
|
||||
attemptSelection();
|
||||
}
|
||||
|
||||
public override void RemoveItem(CarouselItem i)
|
||||
{
|
||||
base.RemoveItem(i);
|
||||
|
||||
if (i != LastSelected)
|
||||
updateSelectedIndex();
|
||||
}
|
||||
|
||||
private bool addingItems;
|
||||
|
||||
public void AddItems(IEnumerable<CarouselItem> items)
|
||||
{
|
||||
addingItems = true;
|
||||
|
||||
foreach (var i in items)
|
||||
AddItem(i);
|
||||
|
||||
addingItems = false;
|
||||
|
||||
attemptSelection();
|
||||
}
|
||||
|
||||
public override void AddItem(CarouselItem i)
|
||||
{
|
||||
base.AddItem(i);
|
||||
if (!addingItems)
|
||||
attemptSelection();
|
||||
}
|
||||
|
||||
protected override void ChildItemStateChanged(CarouselItem item, CarouselItemState value)
|
||||
{
|
||||
base.ChildItemStateChanged(item, value);
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case CarouselItemState.Selected:
|
||||
updateSelected(item);
|
||||
break;
|
||||
|
||||
case CarouselItemState.NotSelected:
|
||||
case CarouselItemState.Collapsed:
|
||||
attemptSelection();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptSelection()
|
||||
{
|
||||
if (DisableSelection) return;
|
||||
|
||||
// we only perform eager selection if we are a currently selected group.
|
||||
if (State.Value != CarouselItemState.Selected) return;
|
||||
|
||||
// we only perform eager selection if none of our items are in a selected state already.
|
||||
if (Items.Any(i => i.State.Value == CarouselItemState.Selected)) return;
|
||||
|
||||
PerformSelection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the item this group would select next if it attempted selection
|
||||
/// </summary>
|
||||
/// <returns>An unfiltered item nearest to the last selected one or null if all items are filtered</returns>
|
||||
public virtual CarouselItem? GetNextToSelect()
|
||||
{
|
||||
if (Items.Count == 0)
|
||||
return null;
|
||||
|
||||
int forwardsIndex = lastSelectedIndex;
|
||||
int backwardsIndex = Math.Min(lastSelectedIndex, Items.Count - 1);
|
||||
|
||||
while (true)
|
||||
{
|
||||
bool hasBackwards = backwardsIndex >= 0 && backwardsIndex < Items.Count;
|
||||
bool hasForwards = forwardsIndex < Items.Count;
|
||||
|
||||
if (!hasBackwards && !hasForwards)
|
||||
return null;
|
||||
|
||||
if (hasForwards && !Items[forwardsIndex].Filtered.Value)
|
||||
return Items[forwardsIndex];
|
||||
|
||||
if (hasBackwards && !Items[backwardsIndex].Filtered.Value)
|
||||
return Items[backwardsIndex];
|
||||
|
||||
forwardsIndex++;
|
||||
backwardsIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void PerformSelection()
|
||||
{
|
||||
CarouselItem? nextToSelect = GetNextToSelect();
|
||||
|
||||
if (nextToSelect != null)
|
||||
nextToSelect.State.Value = CarouselItemState.Selected;
|
||||
else
|
||||
updateSelected(null);
|
||||
}
|
||||
|
||||
private void updateSelected(CarouselItem? newSelection)
|
||||
{
|
||||
if (newSelection != null)
|
||||
LastSelected = newSelection;
|
||||
updateSelectedIndex();
|
||||
}
|
||||
|
||||
private void updateSelectedIndex() => lastSelectedIndex = LastSelected == null ? 0 : Math.Max(0, GetIndexOfItem(LastSelected));
|
||||
}
|
||||
}
|
||||
@@ -1,168 +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 osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public partial class CarouselHeader : Container
|
||||
{
|
||||
public Container BorderContainer;
|
||||
|
||||
public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected);
|
||||
|
||||
private readonly HoverLayer hoverLayer;
|
||||
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private const float corner_radius = 10;
|
||||
private const float border_thickness = 2.5f;
|
||||
|
||||
public CarouselHeader()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = DrawableCarouselItem.MAX_HEIGHT;
|
||||
|
||||
InternalChild = BorderContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = corner_radius,
|
||||
BorderColour = new Color4(221, 255, 255, 255),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Content,
|
||||
hoverLayer = new HoverLayer(),
|
||||
new HeaderSounds(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
State.BindValueChanged(updateState, true);
|
||||
}
|
||||
|
||||
private void updateState(ValueChangedEvent<CarouselItemState> state)
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case CarouselItemState.Collapsed:
|
||||
case CarouselItemState.NotSelected:
|
||||
hoverLayer.InsetForBorder = false;
|
||||
|
||||
BorderContainer.BorderThickness = 0;
|
||||
BorderContainer.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Offset = new Vector2(1),
|
||||
Radius = 10,
|
||||
Colour = Color4.Black.Opacity(100),
|
||||
};
|
||||
break;
|
||||
|
||||
case CarouselItemState.Selected:
|
||||
hoverLayer.InsetForBorder = true;
|
||||
|
||||
BorderContainer.BorderThickness = border_thickness;
|
||||
BorderContainer.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = new Color4(130, 204, 255, 150),
|
||||
Radius = 20,
|
||||
Roundness = 10,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class HoverLayer : CompositeDrawable
|
||||
{
|
||||
private Box box = null!;
|
||||
|
||||
public HoverLayer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
InternalChild = box = new Box
|
||||
{
|
||||
Colour = colours.Blue.Opacity(0.1f),
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
|
||||
public bool InsetForBorder
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
// apply same border as above to avoid applying additive overlay to it (and blowing out the colour).
|
||||
Masking = true;
|
||||
CornerRadius = corner_radius;
|
||||
BorderThickness = border_thickness;
|
||||
}
|
||||
else
|
||||
{
|
||||
BorderThickness = 0;
|
||||
CornerRadius = 0;
|
||||
Masking = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
box.FadeIn(100, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
box.FadeOut(1000, Easing.OutQuint);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
}
|
||||
|
||||
private partial class HeaderSounds : HoverSampleDebounceComponent
|
||||
{
|
||||
private Sample? sampleHover;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
sampleHover = audio.Samples.Get("UI/default-hover");
|
||||
}
|
||||
|
||||
public override void PlayHoverSample()
|
||||
{
|
||||
if (sampleHover == null) return;
|
||||
|
||||
sampleHover.Frequency.Value = 0.99 + RNG.NextDouble(0.02);
|
||||
sampleHover.Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +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;
|
||||
using osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public abstract class CarouselItem : IComparable<CarouselItem>
|
||||
{
|
||||
public virtual float TotalHeight => 0;
|
||||
|
||||
/// <summary>
|
||||
/// An externally defined value used to determine this item's vertical display offset relative to the carousel.
|
||||
/// </summary>
|
||||
public float CarouselYPosition;
|
||||
|
||||
public readonly BindableBool Filtered = new BindableBool();
|
||||
|
||||
public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected);
|
||||
|
||||
/// <summary>
|
||||
/// This item is not in a hidden state.
|
||||
/// </summary>
|
||||
public bool Visible => State.Value != CarouselItemState.Collapsed && !Filtered.Value;
|
||||
|
||||
protected CarouselItem()
|
||||
{
|
||||
Filtered.ValueChanged += filtered =>
|
||||
{
|
||||
if (filtered.NewValue && State.Value == CarouselItemState.Selected)
|
||||
State.Value = CarouselItemState.NotSelected;
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used as a default sort method for <see cref="CarouselItem"/>s of differing types.
|
||||
/// </summary>
|
||||
internal ulong ItemID;
|
||||
|
||||
/// <summary>
|
||||
/// Create a fresh drawable version of this item.
|
||||
/// </summary>
|
||||
public abstract DrawableCarouselItem? CreateDrawableRepresentation();
|
||||
|
||||
public virtual void Filter(FilterCriteria criteria)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual int CompareTo(FilterCriteria criteria, CarouselItem other) => ItemID.CompareTo(other.ItemID);
|
||||
|
||||
public int CompareTo(CarouselItem? other)
|
||||
{
|
||||
if (other == null) return 1;
|
||||
|
||||
return CarouselYPosition.CompareTo(other.CarouselYPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public enum CarouselItemState
|
||||
{
|
||||
Collapsed,
|
||||
NotSelected,
|
||||
Selected,
|
||||
}
|
||||
}
|
||||
@@ -1,321 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using CommonStrings = osu.Game.Localisation.CommonStrings;
|
||||
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public partial class DrawableCarouselBeatmap : DrawableCarouselItem, IHasContextMenu
|
||||
{
|
||||
public const float CAROUSEL_BEATMAP_SPACING = 5;
|
||||
|
||||
/// <summary>
|
||||
/// The height of a carousel beatmap, including vertical spacing.
|
||||
/// </summary>
|
||||
public const float HEIGHT = height + CAROUSEL_BEATMAP_SPACING;
|
||||
|
||||
private const float height = MAX_HEIGHT * 0.6f;
|
||||
|
||||
private readonly BeatmapInfo beatmapInfo;
|
||||
|
||||
private Sprite background = null!;
|
||||
|
||||
private MenuItem[]? mainMenuItems;
|
||||
|
||||
private Action<BeatmapInfo>? selectRequested;
|
||||
private Action<BeatmapInfo>? hideRequested;
|
||||
|
||||
private Triangles triangles = null!;
|
||||
|
||||
private StarCounter starCounter = null!;
|
||||
private DifficultyIcon difficultyIcon = null!;
|
||||
|
||||
private OsuSpriteText keyCountText = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapSetOverlay? beatmapOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager? manager { get; set; }
|
||||
|
||||
private IBindable<StarDifficulty> starDifficultyBindable = null!;
|
||||
private CancellationTokenSource? starDifficultyCancellationSource;
|
||||
|
||||
public DrawableCarouselBeatmap(CarouselBeatmap panel)
|
||||
{
|
||||
beatmapInfo = panel.BeatmapInfo;
|
||||
Item = panel;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SongSelect? songSelect)
|
||||
{
|
||||
Header.Height = height;
|
||||
|
||||
if (songSelect != null)
|
||||
{
|
||||
mainMenuItems = songSelect.CreateForwardNavigationMenuItemsForBeatmap(() => beatmapInfo);
|
||||
selectRequested = b => songSelect.FinaliseSelection(b);
|
||||
}
|
||||
|
||||
if (manager != null)
|
||||
hideRequested = b => manager.Hide(b);
|
||||
|
||||
Header.Children = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
triangles = new Triangles
|
||||
{
|
||||
TriangleScale = 2,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColourLight = Color4Extensions.FromHex(@"3a7285"),
|
||||
ColourDark = Color4Extensions.FromHex(@"123744")
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding(5),
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
difficultyIcon = new DifficultyIcon(beatmapInfo)
|
||||
{
|
||||
TooltipType = DifficultyIconTooltipType.None,
|
||||
Scale = new Vector2(1.8f),
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding { Left = 5 },
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(4, 0),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
keyCountText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 20),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Alpha = 0,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = beatmapInfo.DifficultyName,
|
||||
Font = OsuFont.GetFont(size: 20),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmapInfo.Metadata.Author.Username),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft
|
||||
},
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(4, 0),
|
||||
Scale = new Vector2(0.8f),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new TopLocalRank(beatmapInfo),
|
||||
starCounter = new StarCounter()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.BindValueChanged(_ => updateKeyCount());
|
||||
mods.BindValueChanged(_ => updateKeyCount());
|
||||
}
|
||||
|
||||
protected override void Selected()
|
||||
{
|
||||
base.Selected();
|
||||
|
||||
MovementContainer.MoveToX(-50, 500, Easing.OutExpo);
|
||||
|
||||
background.Colour = ColourInfo.GradientVertical(
|
||||
new Color4(20, 43, 51, 255),
|
||||
new Color4(40, 86, 102, 255));
|
||||
|
||||
triangles.Colour = Color4.White;
|
||||
}
|
||||
|
||||
protected override void Deselected()
|
||||
{
|
||||
base.Deselected();
|
||||
|
||||
MovementContainer.MoveToX(0, 500, Easing.OutExpo);
|
||||
|
||||
background.Colour = new Color4(20, 43, 51, 255);
|
||||
triangles.Colour = OsuColour.Gray(0.5f);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (Item?.State.Value == CarouselItemState.Selected)
|
||||
selectRequested?.Invoke(beatmapInfo);
|
||||
|
||||
return base.OnClick(e);
|
||||
}
|
||||
|
||||
protected override void ApplyState()
|
||||
{
|
||||
if (Item?.State.Value != CarouselItemState.Collapsed && Alpha == 0)
|
||||
starCounter.ReplayAnimation();
|
||||
|
||||
starDifficultyCancellationSource?.Cancel();
|
||||
|
||||
// Only compute difficulty when the item is visible.
|
||||
if (Item?.State.Value != CarouselItemState.Collapsed)
|
||||
{
|
||||
// We've potentially cancelled the computation above so a new bindable is required.
|
||||
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmapInfo, (starDifficultyCancellationSource = new CancellationTokenSource()).Token, 200);
|
||||
starDifficultyBindable.BindValueChanged(d =>
|
||||
{
|
||||
starCounter.Current = (float)(d.NewValue.Stars);
|
||||
difficultyIcon.Current.Value = d.NewValue;
|
||||
}, true);
|
||||
|
||||
updateKeyCount();
|
||||
}
|
||||
|
||||
base.ApplyState();
|
||||
}
|
||||
|
||||
private void updateKeyCount()
|
||||
{
|
||||
if (Item?.State.Value == CarouselItemState.Collapsed)
|
||||
return;
|
||||
|
||||
if (ruleset.Value.OnlineID == 3)
|
||||
{
|
||||
// Account for mania differences locally for now.
|
||||
// Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel.
|
||||
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
|
||||
|
||||
keyCountText.Alpha = 1;
|
||||
keyCountText.Text = $"[{legacyRuleset.GetKeyCount(beatmapInfo, mods.Value)}K]";
|
||||
}
|
||||
else
|
||||
keyCountText.Alpha = 0;
|
||||
}
|
||||
|
||||
public MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
{
|
||||
List<MenuItem> items = new List<MenuItem>();
|
||||
|
||||
if (mainMenuItems != null)
|
||||
items.AddRange(mainMenuItems);
|
||||
|
||||
if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null)
|
||||
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID)));
|
||||
|
||||
var collectionItems = realm.Realm.All<BeatmapCollection>()
|
||||
.OrderBy(c => c.Name)
|
||||
.AsEnumerable()
|
||||
.Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast<OsuMenuItem>().ToList();
|
||||
|
||||
if (manageCollectionsDialog != null)
|
||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||
|
||||
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
|
||||
|
||||
if (beatmapInfo.GetOnlineURL(api, ruleset.Value) is string url)
|
||||
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyToClipboard(url)));
|
||||
|
||||
if (manager != null)
|
||||
items.Add(new OsuMenuItem("Mark as played", MenuItemType.Standard, () => manager.MarkPlayed(beatmapInfo)));
|
||||
|
||||
if (hideRequested != null)
|
||||
items.Add(new OsuMenuItem(WebCommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo)));
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
starDifficultyCancellationSource?.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,356 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public partial class DrawableCarouselBeatmapSet : DrawableCarouselItem, IHasContextMenu
|
||||
{
|
||||
public const float HEIGHT = MAX_HEIGHT;
|
||||
|
||||
private Action<BeatmapSetInfo> restoreHiddenRequested = null!;
|
||||
private Action<int>? viewDetails;
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
public IReadOnlyList<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Array.Empty<DrawableCarouselItem>() : beatmapContainer;
|
||||
|
||||
private Container<DrawableCarouselItem>? beatmapContainer;
|
||||
|
||||
private BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
private Task? beatmapsLoadTask;
|
||||
|
||||
private MenuItem[]? mainMenuItems;
|
||||
|
||||
private double timeSinceUnpool;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager manager { get; set; } = null!;
|
||||
|
||||
protected override void FreeAfterUse()
|
||||
{
|
||||
base.FreeAfterUse();
|
||||
|
||||
Item = null;
|
||||
timeSinceUnpool = 0;
|
||||
|
||||
ClearTransforms();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(BeatmapSetOverlay? beatmapOverlay, SongSelect? songSelect)
|
||||
{
|
||||
if (songSelect != null)
|
||||
mainMenuItems = songSelect.CreateForwardNavigationMenuItemsForBeatmap(() => (((CarouselBeatmapSet)Item!).GetNextToSelect() as CarouselBeatmap)!.BeatmapInfo);
|
||||
|
||||
restoreHiddenRequested = s =>
|
||||
{
|
||||
foreach (var b in s.Beatmaps)
|
||||
manager.Restore(b);
|
||||
};
|
||||
|
||||
if (beatmapOverlay != null)
|
||||
viewDetails = beatmapOverlay.FetchAndShowBeatmapSet;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
// position updates should not occur if the item is filtered away.
|
||||
// this avoids panels flying across the screen only to be eventually off-screen or faded out.
|
||||
if (!Item.Visible) return;
|
||||
|
||||
float targetY = Item.CarouselYPosition;
|
||||
|
||||
if (Precision.AlmostEquals(targetY, Y))
|
||||
Y = targetY;
|
||||
else
|
||||
// algorithm for this is taken from ScrollContainer.
|
||||
// while it doesn't necessarily need to match 1:1, as we are emulating scroll in some cases this feels most correct.
|
||||
Y = (float)Interpolation.Lerp(targetY, Y, Math.Exp(-0.01 * Time.Elapsed));
|
||||
|
||||
loadContentIfRequired();
|
||||
}
|
||||
|
||||
private CancellationTokenSource? loadCancellation;
|
||||
|
||||
protected override void UpdateItem()
|
||||
{
|
||||
loadCancellation?.Cancel();
|
||||
loadCancellation = null;
|
||||
|
||||
base.UpdateItem();
|
||||
|
||||
Content.Clear();
|
||||
Header.Clear();
|
||||
|
||||
beatmapContainer = null;
|
||||
beatmapsLoadTask = null;
|
||||
|
||||
if (Item == null)
|
||||
return;
|
||||
|
||||
beatmapSet = ((CarouselBeatmapSet)Item).BeatmapSet;
|
||||
}
|
||||
|
||||
protected override void Deselected()
|
||||
{
|
||||
base.Deselected();
|
||||
|
||||
MovementContainer.MoveToX(0, 500, Easing.OutExpo);
|
||||
|
||||
updateBeatmapYPositions();
|
||||
}
|
||||
|
||||
protected override void Selected()
|
||||
{
|
||||
base.Selected();
|
||||
|
||||
MovementContainer.MoveToX(-100, 500, Easing.OutExpo);
|
||||
|
||||
updateBeatmapDifficulties();
|
||||
}
|
||||
|
||||
private void updateBeatmapDifficulties()
|
||||
{
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
var carouselBeatmapSet = (CarouselBeatmapSet)Item;
|
||||
|
||||
var visibleBeatmaps = carouselBeatmapSet.Items.Where(c => c.Visible).ToArray();
|
||||
|
||||
// if we are already displaying all the correct beatmaps, only run animation updates.
|
||||
// note that the displayed beatmaps may change due to the applied filter.
|
||||
// a future optimisation could add/remove only changed difficulties rather than reinitialise.
|
||||
if (beatmapContainer != null && visibleBeatmaps.Length == beatmapContainer.Count && visibleBeatmaps.All(b => beatmapContainer.Any(c => c.Item == b)))
|
||||
{
|
||||
updateBeatmapYPositions();
|
||||
}
|
||||
else
|
||||
{
|
||||
// on selection we show our child beatmaps.
|
||||
// for now this is a simple drawable construction each selection.
|
||||
// can be improved in the future.
|
||||
beatmapContainer = new Container<DrawableCarouselItem>
|
||||
{
|
||||
X = 100,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation()!)
|
||||
};
|
||||
|
||||
beatmapsLoadTask = LoadComponentAsync(beatmapContainer, loaded =>
|
||||
{
|
||||
// make sure the pooled target hasn't changed.
|
||||
if (beatmapContainer != loaded)
|
||||
return;
|
||||
|
||||
Content.Child = loaded;
|
||||
updateBeatmapYPositions();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private BeatmapCarousel.CarouselScrollContainer scrollContainer { get; set; } = null!;
|
||||
|
||||
private void loadContentIfRequired()
|
||||
{
|
||||
Quad containingSsdq = scrollContainer.ScreenSpaceDrawQuad;
|
||||
|
||||
// Using DelayedLoadWrappers would only allow us to load content when on screen, but we want to preload while off-screen
|
||||
// to provide a better user experience.
|
||||
|
||||
// This is tracking time that this drawable is updating since the last pool.
|
||||
// This is intended to provide a debounce so very fast scrolls (from one end to the other of the carousel)
|
||||
// don't cause huge overheads.
|
||||
//
|
||||
// We increase the delay based on distance from centre, so the beatmaps the user is currently looking at load first.
|
||||
float timeUpdatingBeforeLoad = 50 + Math.Abs(containingSsdq.Centre.Y - ScreenSpaceDrawQuad.Centre.Y) / containingSsdq.Height * 100;
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
// A load is already in progress if the cancellation token is non-null.
|
||||
if (loadCancellation != null)
|
||||
return;
|
||||
|
||||
timeSinceUnpool += Time.Elapsed;
|
||||
|
||||
// We only trigger a load after this set has been in an updating state for a set amount of time.
|
||||
if (timeSinceUnpool <= timeUpdatingBeforeLoad)
|
||||
return;
|
||||
|
||||
loadCancellation = new CancellationTokenSource();
|
||||
|
||||
LoadComponentsAsync(new CompositeDrawable[]
|
||||
{
|
||||
// Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set).
|
||||
new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new SetPanelContent((CarouselBeatmapSet)Item)
|
||||
{
|
||||
Depth = float.MinValue,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}, drawables =>
|
||||
{
|
||||
Header.AddRange(drawables);
|
||||
drawables.ForEach(d => d.FadeInFromZero(150));
|
||||
}, loadCancellation.Token);
|
||||
}
|
||||
|
||||
private void updateBeatmapYPositions()
|
||||
{
|
||||
if (beatmapContainer == null)
|
||||
return;
|
||||
|
||||
if (beatmapsLoadTask == null || !beatmapsLoadTask.IsCompleted)
|
||||
return;
|
||||
|
||||
float yPos = DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING;
|
||||
|
||||
bool isSelected = Item?.State.Value == CarouselItemState.Selected;
|
||||
|
||||
foreach (var panel in beatmapContainer)
|
||||
{
|
||||
Debug.Assert(panel.Item != null);
|
||||
|
||||
if (isSelected)
|
||||
{
|
||||
panel.MoveToY(yPos, 800, Easing.OutQuint);
|
||||
yPos += panel.Item.TotalHeight;
|
||||
}
|
||||
else
|
||||
panel.MoveToY(0, 800, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
public MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
{
|
||||
Debug.Assert(beatmapSet != null);
|
||||
|
||||
List<MenuItem> items = new List<MenuItem>();
|
||||
|
||||
if (Item?.State.Value == CarouselItemState.NotSelected)
|
||||
items.Add(new OsuMenuItem("Expand", MenuItemType.Highlighted, () => Item.State.Value = CarouselItemState.Selected));
|
||||
|
||||
if (mainMenuItems != null)
|
||||
items.AddRange(mainMenuItems);
|
||||
|
||||
if (beatmapSet.OnlineID > 0 && viewDetails != null)
|
||||
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID)));
|
||||
|
||||
var collectionItems = realm.Realm.All<BeatmapCollection>()
|
||||
.OrderBy(c => c.Name)
|
||||
.AsEnumerable()
|
||||
.Select(createCollectionMenuItem)
|
||||
.ToList();
|
||||
|
||||
if (manageCollectionsDialog != null)
|
||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||
|
||||
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
|
||||
|
||||
if (beatmapSet.Beatmaps.Any(b => b.Hidden))
|
||||
items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet)));
|
||||
|
||||
if (beatmapSet.GetOnlineURL(api, ruleset.Value) is string url)
|
||||
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyToClipboard(url)));
|
||||
|
||||
if (dialogOverlay != null)
|
||||
items.Add(new OsuMenuItem("Delete...", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet))));
|
||||
return items.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private MenuItem createCollectionMenuItem(BeatmapCollection collection)
|
||||
{
|
||||
Debug.Assert(beatmapSet != null);
|
||||
|
||||
TernaryState state;
|
||||
|
||||
int countExisting = beatmapSet.Beatmaps.Count(b => collection.BeatmapMD5Hashes.Contains(b.MD5Hash));
|
||||
|
||||
if (countExisting == beatmapSet.Beatmaps.Count)
|
||||
state = TernaryState.True;
|
||||
else if (countExisting > 0)
|
||||
state = TernaryState.Indeterminate;
|
||||
else
|
||||
state = TernaryState.False;
|
||||
|
||||
var liveCollection = collection.ToLive(realm);
|
||||
|
||||
return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s =>
|
||||
{
|
||||
Task.Run(() => liveCollection.PerformWrite(c =>
|
||||
{
|
||||
foreach (var b in beatmapSet.Beatmaps)
|
||||
{
|
||||
switch (s)
|
||||
{
|
||||
case TernaryState.True:
|
||||
if (c.BeatmapMD5Hashes.Contains(b.MD5Hash))
|
||||
continue;
|
||||
|
||||
c.BeatmapMD5Hashes.Add(b.MD5Hash);
|
||||
break;
|
||||
|
||||
case TernaryState.False:
|
||||
c.BeatmapMD5Hashes.Remove(b.MD5Hash);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}));
|
||||
})
|
||||
{
|
||||
State = { Value = state }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,177 +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.Diagnostics;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Input.Events;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public abstract partial class DrawableCarouselItem : PoolableDrawable
|
||||
{
|
||||
public const float MAX_HEIGHT = 80;
|
||||
|
||||
public override bool IsPresent => base.IsPresent || Item?.Visible == true;
|
||||
|
||||
public override bool HandlePositionalInput => Item?.Visible == true;
|
||||
public override bool PropagatePositionalInputSubTree => Item?.Visible == true;
|
||||
|
||||
public readonly CarouselHeader Header;
|
||||
|
||||
/// <summary>
|
||||
/// Optional content which sits below the header.
|
||||
/// </summary>
|
||||
protected readonly Container<Drawable> Content;
|
||||
|
||||
protected readonly Container MovementContainer;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
Header.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
private CarouselItem? item;
|
||||
|
||||
public CarouselItem? Item
|
||||
{
|
||||
get => item;
|
||||
set
|
||||
{
|
||||
if (item == value)
|
||||
return;
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
item.Filtered.ValueChanged -= onStateChange;
|
||||
item.State.ValueChanged -= onStateChange;
|
||||
|
||||
Header.State.UnbindFrom(item.State);
|
||||
|
||||
if (item is CarouselGroup group)
|
||||
{
|
||||
foreach (var c in group.Items)
|
||||
c.Filtered.ValueChanged -= onStateChange;
|
||||
}
|
||||
}
|
||||
|
||||
item = value;
|
||||
|
||||
if (IsLoaded && !IsDisposed)
|
||||
UpdateItem();
|
||||
}
|
||||
}
|
||||
|
||||
protected DrawableCarouselItem()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
Alpha = 0;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
MovementContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Header = new CarouselHeader(),
|
||||
Content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
UpdateItem();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
Content.Y = Header.Height;
|
||||
}
|
||||
|
||||
protected virtual void UpdateItem()
|
||||
{
|
||||
if (Item == null)
|
||||
return;
|
||||
|
||||
Scheduler.AddOnce(ApplyState);
|
||||
|
||||
Item.Filtered.ValueChanged += onStateChange;
|
||||
Item.State.ValueChanged += onStateChange;
|
||||
|
||||
Header.State.BindTo(Item.State);
|
||||
|
||||
if (Item is CarouselGroup group)
|
||||
{
|
||||
foreach (var c in group.Items)
|
||||
c.Filtered.ValueChanged += onStateChange;
|
||||
}
|
||||
}
|
||||
|
||||
private void onStateChange(ValueChangedEvent<CarouselItemState> obj) => Scheduler.AddOnce(ApplyState);
|
||||
|
||||
private void onStateChange(ValueChangedEvent<bool> _) => Scheduler.AddOnce(ApplyState);
|
||||
|
||||
protected virtual void ApplyState()
|
||||
{
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
// Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead.
|
||||
// Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away.
|
||||
Height = Item.TotalHeight;
|
||||
|
||||
switch (Item.State.Value)
|
||||
{
|
||||
case CarouselItemState.NotSelected:
|
||||
Deselected();
|
||||
break;
|
||||
|
||||
case CarouselItemState.Selected:
|
||||
Selected();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!Item.Visible)
|
||||
this.FadeOut(100, Easing.OutQuint);
|
||||
else
|
||||
this.FadeIn(400, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected virtual void Selected()
|
||||
{
|
||||
Debug.Assert(Item != null);
|
||||
}
|
||||
|
||||
protected virtual void Deselected()
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
Item.State.Value = CarouselItemState.Selected;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e) => true;
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
// This is important to clean up event subscriptions.
|
||||
Item = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +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 osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public partial class FilterableDifficultyIcon : DifficultyIcon
|
||||
{
|
||||
private readonly BindableBool filtered = new BindableBool();
|
||||
|
||||
public bool IsFiltered => filtered.Value;
|
||||
|
||||
public readonly CarouselBeatmap Item;
|
||||
|
||||
public FilterableDifficultyIcon(CarouselBeatmap item)
|
||||
: base(item.BeatmapInfo)
|
||||
{
|
||||
filtered.BindTo(item.Filtered);
|
||||
filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100));
|
||||
filtered.TriggerChange();
|
||||
|
||||
Item = item;
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
Item.State.Value = CarouselItemState.Selected;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +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 osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
/// <summary>
|
||||
/// A difficulty icon that contains a counter on the right-side of it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used in cases when there are too many difficulty icons to show.
|
||||
/// </remarks>
|
||||
public partial class GroupedDifficultyIcon : DifficultyIcon
|
||||
{
|
||||
public readonly List<CarouselBeatmap> Items;
|
||||
|
||||
public GroupedDifficultyIcon(List<CarouselBeatmap> items, RulesetInfo ruleset)
|
||||
: base(items.OrderBy(b => b.BeatmapInfo.StarRating).Last().BeatmapInfo, ruleset)
|
||||
{
|
||||
Items = items;
|
||||
|
||||
foreach (var item in items)
|
||||
item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay));
|
||||
|
||||
AddInternal(new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Padding = new MarginPadding { Left = Size.X },
|
||||
Margin = new MarginPadding { Left = 2, Right = 5 },
|
||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
|
||||
Text = items.Count.ToString(),
|
||||
Colour = Color4.White,
|
||||
});
|
||||
|
||||
updateFilteredDisplay();
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
Items.First().State.Value = CarouselItemState.Selected;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateFilteredDisplay()
|
||||
{
|
||||
// for now, fade the whole group based on the ratio of hidden items.
|
||||
this.FadeTo(1 - 0.9f * ((float)Items.Count(i => i.Filtered.Value) / Items.Count), 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +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;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public partial class SetPanelBackground : BufferedContainer
|
||||
{
|
||||
public SetPanelBackground(IWorkingBeatmap working)
|
||||
: base(cachedFrameBuffer: true)
|
||||
{
|
||||
RedrawOnScale = false;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new PanelBeatmapBackground(working)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FillMode = FillMode.Fill,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Depth = -1,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
// This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle
|
||||
Shear = new Vector2(0.8f, 0),
|
||||
Alpha = 0.5f,
|
||||
Children = new[]
|
||||
{
|
||||
// The left half with no gradient applied
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
Width = 0.4f,
|
||||
},
|
||||
// Piecewise-linear gradient with 3 segments to make it appear smoother
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)),
|
||||
Width = 0.05f,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)),
|
||||
Width = 0.2f,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)),
|
||||
Width = 0.05f,
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public partial class PanelBeatmapBackground : Sprite
|
||||
{
|
||||
private readonly IWorkingBeatmap working;
|
||||
|
||||
public PanelBeatmapBackground(IWorkingBeatmap working)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(working);
|
||||
|
||||
this.working = working;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Texture = working.GetPanelBackground();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public partial class SetPanelContent : CompositeDrawable
|
||||
{
|
||||
// Disallow interacting with difficulty icons on a panel until the panel has been selected.
|
||||
public override bool PropagatePositionalInputSubTree => carouselSet.State.Value == CarouselItemState.Selected;
|
||||
|
||||
private readonly CarouselBeatmapSet carouselSet;
|
||||
|
||||
private FillFlowContainer<DifficultyIcon> iconFlow = null!;
|
||||
|
||||
public SetPanelContent(CarouselBeatmapSet carouselSet)
|
||||
{
|
||||
this.carouselSet = carouselSet;
|
||||
|
||||
// required to ensure we load as soon as any part of the panel comes on screen
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var beatmapSet = carouselSet.BeatmapSet;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
// required to ensure we load as soon as any part of the panel comes on screen
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding { Top = 5, Left = 18, Right = 10, Bottom = 10 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title),
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
|
||||
Shadow = true,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist),
|
||||
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
|
||||
Shadow = true,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 5 },
|
||||
Spacing = new Vector2(5),
|
||||
Children = new[]
|
||||
{
|
||||
beatmapSet.AllBeatmapsUpToDate
|
||||
? Empty()
|
||||
: new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new UpdateBeatmapSetButton(beatmapSet),
|
||||
}
|
||||
},
|
||||
new BeatmapSetOnlineStatusPill
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
TextSize = 11,
|
||||
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
|
||||
Status = beatmapSet.Status
|
||||
},
|
||||
iconFlow = new FillFlowContainer<DifficultyIcon>
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Spacing = new Vector2(3),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
iconFlow.ChildrenEnumerable = getDifficultyIcons();
|
||||
}
|
||||
|
||||
private const int maximum_difficulty_icons = 18;
|
||||
|
||||
private IEnumerable<DifficultyIcon> getDifficultyIcons()
|
||||
{
|
||||
var beatmaps = carouselSet.Beatmaps.ToList();
|
||||
|
||||
return beatmaps.Count > maximum_difficulty_icons
|
||||
? beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset)
|
||||
.Select(group => new GroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset))
|
||||
: beatmaps.Select(b => new FilterableDifficultyIcon(b));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +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;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public partial class TopLocalRank : CompositeDrawable
|
||||
{
|
||||
private readonly BeatmapInfo beatmapInfo;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private IDisposable? scoreSubscription;
|
||||
|
||||
private readonly UpdateableRank updateable;
|
||||
|
||||
public ScoreRank? DisplayedRank => updateable.Rank;
|
||||
|
||||
public TopLocalRank(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
this.beatmapInfo = beatmapInfo;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = updateable = new UpdateableRank
|
||||
{
|
||||
Size = new Vector2(40, 20),
|
||||
Alpha = 0,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.BindValueChanged(_ =>
|
||||
{
|
||||
scoreSubscription?.Dispose();
|
||||
scoreSubscription = realm.RegisterForNotifications(r =>
|
||||
r.GetAllLocalScoresForUser(api.LocalUser.Value.Id)
|
||||
.Filter($@"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0"
|
||||
+ $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1", beatmapInfo.ID, ruleset.Value.ShortName),
|
||||
localScoresChanged);
|
||||
}, true);
|
||||
|
||||
void localScoresChanged(IRealmCollection<ScoreInfo> sender, ChangeSet? changes)
|
||||
{
|
||||
// This subscription may fire from changes to linked beatmaps, which we don't care about.
|
||||
// It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications.
|
||||
if (changes?.HasCollectionChanges() == false)
|
||||
return;
|
||||
|
||||
ScoreInfo? topScore = sender.MaxBy(info => (info.TotalScore, -info.Date.UtcDateTime.Ticks));
|
||||
updateable.Rank = topScore?.Rank;
|
||||
updateable.Alpha = topScore != null ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
scoreSubscription?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,178 +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 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.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
public partial class UpdateBeatmapSetButton : OsuAnimatedButton
|
||||
{
|
||||
private readonly BeatmapSetInfo beatmapSetInfo;
|
||||
private SpriteIcon icon = null!;
|
||||
private Box progressFill = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private LoginOverlay? loginOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
public UpdateBeatmapSetButton(BeatmapSetInfo beatmapSetInfo)
|
||||
{
|
||||
this.beatmapSetInfo = beatmapSetInfo;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Anchor = Anchor.CentreLeft;
|
||||
Origin = Anchor.CentreLeft;
|
||||
}
|
||||
|
||||
private Bindable<bool> preferNoVideo = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
const float icon_size = 14;
|
||||
|
||||
preferNoVideo = config.GetBindable<bool>(OsuSetting.PreferNoVideo);
|
||||
|
||||
Content.Anchor = Anchor.CentreLeft;
|
||||
Content.Origin = Anchor.CentreLeft;
|
||||
|
||||
Content.AddRange(new Drawable[]
|
||||
{
|
||||
progressFill = new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
Alpha = 0.2f,
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding { Horizontal = 5, Vertical = 3 },
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(4),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Size = new Vector2(icon_size),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
icon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.SyncAlt,
|
||||
Size = new Vector2(icon_size),
|
||||
},
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.Default.With(weight: FontWeight.Bold),
|
||||
Text = "Update",
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Action = updateBeatmap;
|
||||
}
|
||||
|
||||
private bool updateConfirmed;
|
||||
|
||||
private void updateBeatmap()
|
||||
{
|
||||
if (!api.IsLoggedIn)
|
||||
{
|
||||
loginOverlay?.Show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dialogOverlay != null && beatmapSetInfo.Status == BeatmapOnlineStatus.LocallyModified && !updateConfirmed)
|
||||
{
|
||||
dialogOverlay.Push(new UpdateLocalConfirmationDialog(() =>
|
||||
{
|
||||
updateConfirmed = true;
|
||||
updateBeatmap();
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateConfirmed = false;
|
||||
|
||||
beatmapDownloader.DownloadAsUpdate(beatmapSetInfo, preferNoVideo.Value);
|
||||
attachExistingDownload();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
icon.Spin(4000, RotationDirection.Clockwise);
|
||||
}
|
||||
|
||||
private void attachExistingDownload()
|
||||
{
|
||||
var download = beatmapDownloader.GetExistingDownload(beatmapSetInfo);
|
||||
|
||||
if (download != null)
|
||||
{
|
||||
Enabled.Value = false;
|
||||
TooltipText = string.Empty;
|
||||
|
||||
download.DownloadProgressed += progress => progressFill.ResizeWidthTo(progress, 100, Easing.OutQuint);
|
||||
download.Failure += _ => attachExistingDownload();
|
||||
}
|
||||
else
|
||||
{
|
||||
Enabled.Value = true;
|
||||
TooltipText = "Update beatmap with online changes";
|
||||
|
||||
progressFill.ResizeWidthTo(0, 100, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
icon.Spin(400, RotationDirection.Clockwise, icon.Rotation);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
icon.Spin(4000, RotationDirection.Clockwise, icon.Rotation);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,309 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
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.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class FilterControl : Container
|
||||
{
|
||||
public const float HEIGHT = 2 * side_margin + 120;
|
||||
|
||||
private const float side_margin = 10;
|
||||
|
||||
public Action<FilterCriteria> FilterChanged;
|
||||
|
||||
public Bindable<string> CurrentTextSearch => searchTextBox.Current;
|
||||
|
||||
public LocalisableString InformationalText
|
||||
{
|
||||
get => searchTextBox.FilterText.Text;
|
||||
set => searchTextBox.FilterText.Text = value;
|
||||
}
|
||||
|
||||
private OsuTabControl<SortMode> sortTabs;
|
||||
private Bindable<SortMode> sortMode;
|
||||
private Bindable<GroupMode> groupMode;
|
||||
private FilterControlTextBox searchTextBox;
|
||||
private CollectionDropdown collectionDropdown;
|
||||
|
||||
[CanBeNull]
|
||||
private FilterCriteria currentCriteria;
|
||||
|
||||
public virtual FilterCriteria CreateCriteria()
|
||||
{
|
||||
string query = searchTextBox.Text;
|
||||
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Group = groupMode.Value,
|
||||
Sort = sortMode.Value,
|
||||
AllowConvertedBeatmaps = showConverted.Value,
|
||||
Ruleset = ruleset.Value,
|
||||
Mods = mods.Value,
|
||||
CollectionBeatmapMD5Hashes = collectionDropdown.Current.Value?.Collection?.PerformRead(c => c.BeatmapMD5Hashes).ToImmutableHashSet()
|
||||
};
|
||||
|
||||
if (!minimumStars.IsDefault)
|
||||
criteria.UserStarDifficulty.Min = minimumStars.Value;
|
||||
|
||||
if (!maximumStars.IsDefault)
|
||||
criteria.UserStarDifficulty.Max = maximumStars.Value;
|
||||
|
||||
criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria();
|
||||
|
||||
FilterQueryParser.ApplyQueries(criteria, query);
|
||||
return criteria;
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
base.ReceivePositionalInputAt(screenSpacePos) || sortTabs.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
[BackgroundDependencyLoader(permitNulls: true)]
|
||||
private void load(OsuColour colours, OsuConfigManager config)
|
||||
{
|
||||
sortMode = config.GetBindable<SortMode>(OsuSetting.SongSelectSortingMode);
|
||||
groupMode = config.GetBindable<GroupMode>(OsuSetting.SongSelectGroupMode);
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = OsuColour.Gray(0.05f),
|
||||
Alpha = 0.96f,
|
||||
Width = 2,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Padding = new MarginPadding(side_margin),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
// Reverse ChildID so that dropdowns in the top section appear on top of the bottom section.
|
||||
Child = new ReverseChildIDFillFlowContainer<Drawable>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(0, 5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
searchTextBox = new FilterControlTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 1,
|
||||
Colour = OsuColour.Gray(80),
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Absolute, OsuTabControl<SortMode>.HORIZONTAL_SPACING),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, OsuTabControl<SortMode>.HORIZONTAL_SPACING),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = SortStrings.Default,
|
||||
Font = OsuFont.GetFont(size: 14),
|
||||
Margin = new MarginPadding(5),
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
},
|
||||
Empty(),
|
||||
sortTabs = new OsuTabControl<SortMode>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 24,
|
||||
AutoSort = true,
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
AccentColour = colours.GreenLight,
|
||||
Current = { BindTarget = sortMode }
|
||||
},
|
||||
Empty(),
|
||||
new OsuTabControlCheckbox
|
||||
{
|
||||
Text = "Show converted",
|
||||
Current = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps),
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 40,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new RangeSlider
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Label = "Difficulty range",
|
||||
LowerBound = config.GetBindable<double>(OsuSetting.DisplayStarsMinimum),
|
||||
UpperBound = config.GetBindable<double>(OsuSetting.DisplayStarsMaximum),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.48f,
|
||||
DefaultStringLowerBound = "0",
|
||||
DefaultStringUpperBound = "∞",
|
||||
DefaultTooltipUpperBound = UserInterfaceStrings.NoLimit,
|
||||
TooltipSuffix = "stars"
|
||||
},
|
||||
collectionDropdown = new CollectionDropdown
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RequestFilter = updateCriteria,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Y = 4,
|
||||
Width = 0.5f,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConverted);
|
||||
showConverted.ValueChanged += _ => updateCriteria();
|
||||
|
||||
config.BindWith(OsuSetting.DisplayStarsMinimum, minimumStars);
|
||||
minimumStars.ValueChanged += _ => updateCriteria();
|
||||
|
||||
config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars);
|
||||
maximumStars.ValueChanged += _ => updateCriteria();
|
||||
|
||||
ruleset.BindValueChanged(_ => updateCriteria());
|
||||
mods.BindValueChanged(m =>
|
||||
{
|
||||
// Mods are updated once by the mod select overlay when song select is entered,
|
||||
// regardless of if there are any mods or any changes have taken place.
|
||||
// Updating the criteria here so early triggers a re-ordering of panels on song select, via... some mechanism.
|
||||
// Todo: Investigate/fix and potentially remove this.
|
||||
if (m.NewValue.SequenceEqual(m.OldValue))
|
||||
return;
|
||||
|
||||
if (currentCriteria?.RulesetCriteria?.FilterMayChangeFromMods(m) == true)
|
||||
updateCriteria();
|
||||
});
|
||||
|
||||
groupMode.BindValueChanged(_ => updateCriteria());
|
||||
sortMode.BindValueChanged(_ => updateCriteria());
|
||||
|
||||
searchTextBox.Current.ValueChanged += _ => updateCriteria();
|
||||
|
||||
updateCriteria();
|
||||
}
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
searchTextBox.ReadOnly = true;
|
||||
searchTextBox.HoldFocus = false;
|
||||
if (searchTextBox.HasFocus)
|
||||
GetContainingFocusManager()!.ChangeFocus(searchTextBox);
|
||||
}
|
||||
|
||||
public void Activate()
|
||||
{
|
||||
searchTextBox.ReadOnly = false;
|
||||
searchTextBox.HoldFocus = true;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
private readonly Bindable<bool> showConverted = new Bindable<bool>();
|
||||
private readonly Bindable<double> minimumStars = new BindableDouble();
|
||||
private readonly Bindable<double> maximumStars = new BindableDouble();
|
||||
|
||||
private void updateCriteria() => FilterChanged?.Invoke(currentCriteria = CreateCriteria());
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => true;
|
||||
|
||||
protected override bool OnHover(HoverEvent e) => true;
|
||||
|
||||
internal partial class FilterControlTextBox : SeekLimitedSearchTextBox
|
||||
{
|
||||
private const float filter_text_size = 12;
|
||||
|
||||
public OsuSpriteText FilterText { get; private set; }
|
||||
|
||||
public FilterControlTextBox()
|
||||
{
|
||||
Height += filter_text_size;
|
||||
TextContainer.Height *= (Height - filter_text_size) / Height;
|
||||
TextContainer.Margin = new MarginPadding { Bottom = filter_text_size };
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
TextContainer.Add(FilterText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Depth = float.MinValue,
|
||||
Font = OsuFont.Default.With(size: filter_text_size, weight: FontWeight.SemiBold),
|
||||
Margin = new MarginPadding { Top = 2, Left = 2 },
|
||||
Colour = colours.Yellow
|
||||
});
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||
{
|
||||
// the "cut" platform key binding (shift-delete) conflicts with the beatmap deletion action.
|
||||
if (e.Action == PlatformAction.Cut && e.ShiftPressed && e.CurrentState.Keyboard.Keys.IsPressed(Key.Delete))
|
||||
return false;
|
||||
|
||||
return base.OnPressed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,124 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osuTK;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class Footer : Container
|
||||
{
|
||||
private readonly Box modeLight;
|
||||
|
||||
public const float HEIGHT = 50;
|
||||
|
||||
public const int TRANSITION_LENGTH = 300;
|
||||
|
||||
private const float padding = 80;
|
||||
|
||||
private readonly FillFlowContainer<FooterButton> buttons;
|
||||
|
||||
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
|
||||
|
||||
/// <param name="button">The button to be added.</param>
|
||||
/// <param name="overlay">The <see cref="OverlayContainer"/> to be toggled by this button.</param>
|
||||
public void AddButton(FooterButton button, OverlayContainer overlay)
|
||||
{
|
||||
if (overlay != null)
|
||||
{
|
||||
overlays.Add(overlay);
|
||||
button.Action = () => showOverlay(overlay);
|
||||
}
|
||||
|
||||
button.Hovered = updateModeLight;
|
||||
button.HoverLost = updateModeLight;
|
||||
|
||||
buttons.Add(button);
|
||||
}
|
||||
|
||||
private void showOverlay(OverlayContainer overlay)
|
||||
{
|
||||
foreach (var o in overlays)
|
||||
{
|
||||
if (o == overlay)
|
||||
o.ToggleVisibility();
|
||||
else
|
||||
o.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateModeLight()
|
||||
{
|
||||
var selectedButton = buttons.FirstOrDefault(b => b.Enabled.Value && b.IsHovered);
|
||||
|
||||
if (selectedButton != null)
|
||||
{
|
||||
modeLight.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
modeLight.FadeColour(selectedButton.SelectedColour, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
modeLight.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public Footer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = HEIGHT;
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One,
|
||||
Colour = OsuColour.Gray(0.1f),
|
||||
Alpha = 0.96f,
|
||||
},
|
||||
modeLight = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 3,
|
||||
Position = new Vector2(0, -3),
|
||||
Colour = OsuColour.Gray(0.1f),
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Position = new Vector2(TwoLayerButton.SIZE_EXTENDED.X + padding, 0),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(padding, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
buttons = new FillFlowContainer<FooterButton>
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(-FooterButton.SHEAR_WIDTH, 0),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateModeLight();
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => true;
|
||||
|
||||
protected override bool OnHover(HoverEvent e) => true;
|
||||
}
|
||||
}
|
||||
@@ -1,234 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class FooterButton : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
public const float SHEAR_WIDTH = 7.5f;
|
||||
|
||||
protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Used to show an initial animation hinting at the enabled state.
|
||||
/// </summary>
|
||||
protected virtual bool IsActive => false;
|
||||
|
||||
public LocalisableString Text
|
||||
{
|
||||
get => SpriteText?.Text ?? default;
|
||||
set
|
||||
{
|
||||
if (SpriteText != null)
|
||||
SpriteText.Text = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Color4 deselectedColour;
|
||||
|
||||
public Color4 DeselectedColour
|
||||
{
|
||||
get => deselectedColour;
|
||||
set
|
||||
{
|
||||
deselectedColour = value;
|
||||
if (light.Colour != SelectedColour)
|
||||
light.Colour = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Color4 selectedColour;
|
||||
|
||||
public Color4 SelectedColour
|
||||
{
|
||||
get => selectedColour;
|
||||
set
|
||||
{
|
||||
selectedColour = value;
|
||||
box.Colour = selectedColour;
|
||||
}
|
||||
}
|
||||
|
||||
protected FillFlowContainer ButtonContentContainer;
|
||||
protected readonly Container TextContainer;
|
||||
protected readonly SpriteText SpriteText;
|
||||
private readonly Box box;
|
||||
private readonly Box light;
|
||||
|
||||
public FooterButton()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Shear = SHEAR;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
box = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
EdgeSmoothness = new Vector2(2, 0),
|
||||
Colour = Color4.White,
|
||||
Alpha = 0,
|
||||
},
|
||||
light = new Box
|
||||
{
|
||||
Height = 4,
|
||||
EdgeSmoothness = new Vector2(2, 0),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
ButtonContentContainer = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Shear = -SHEAR,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Height = 50,
|
||||
Spacing = new Vector2(15, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
TextContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = SpriteText = new OsuSpriteText
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Enabled.BindValueChanged(_ => updateDisplay(), true);
|
||||
|
||||
if (IsActive)
|
||||
{
|
||||
box.ClearTransforms();
|
||||
|
||||
using (box.BeginDelayedSequence(200))
|
||||
{
|
||||
box.FadeIn(200)
|
||||
.Then()
|
||||
.FadeOut(1500, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Action Hovered;
|
||||
public Action HoverLost;
|
||||
public GlobalAction? Hotkey;
|
||||
|
||||
private bool mouseDown;
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
float horizontalMargin = (100 - TextContainer.Width) / 2;
|
||||
ButtonContentContainer.Padding = new MarginPadding
|
||||
{
|
||||
Left = horizontalMargin,
|
||||
// right side margin offset to compensate for shear
|
||||
Right = horizontalMargin - SHEAR_WIDTH / 2
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
Hovered?.Invoke();
|
||||
updateDisplay();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
HoverLost?.Invoke();
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (!Enabled.Value)
|
||||
return true;
|
||||
|
||||
mouseDown = true;
|
||||
updateDisplay();
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
mouseDown = false;
|
||||
updateDisplay();
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (!Enabled.Value)
|
||||
return true;
|
||||
|
||||
box.ClearTransforms();
|
||||
box.Alpha = 1;
|
||||
box.FadeOut(Footer.TRANSITION_LENGTH * 3, Easing.OutQuint);
|
||||
return base.OnClick(e);
|
||||
}
|
||||
|
||||
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Action == Hotkey && !e.Repeat)
|
||||
{
|
||||
TriggerClick();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) { }
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
this.FadeTo(Enabled.Value ? 1 : 0.25f, Footer.TRANSITION_LENGTH, Easing.OutQuint);
|
||||
|
||||
light.ScaleTo(Enabled.Value && IsHovered ? new Vector2(1, 2) : new Vector2(1), Footer.TRANSITION_LENGTH, Easing.OutQuint);
|
||||
light.FadeColour(Enabled.Value && IsHovered ? SelectedColour : DeselectedColour, Footer.TRANSITION_LENGTH, Easing.OutQuint);
|
||||
|
||||
box.FadeTo(Enabled.Value & mouseDown ? 0.3f : 0f, Footer.TRANSITION_LENGTH * 2, Easing.OutQuint);
|
||||
|
||||
if (Enabled.Value && IsHovered)
|
||||
Hovered?.Invoke();
|
||||
else
|
||||
HoverLost?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +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 osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class FooterButtonMods : FooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
|
||||
{
|
||||
public Bindable<IReadOnlyList<Mod>> Current
|
||||
{
|
||||
get => modDisplay.Current;
|
||||
set => modDisplay.Current = value;
|
||||
}
|
||||
|
||||
protected OsuSpriteText MultiplierText { get; private set; } = null!;
|
||||
protected Container UnrankedBadge { get; private set; } = null!;
|
||||
|
||||
private readonly ModDisplay modDisplay;
|
||||
|
||||
private ModSettingChangeTracker? modSettingChangeTracker;
|
||||
|
||||
private Color4 lowMultiplierColour;
|
||||
private Color4 highMultiplierColour;
|
||||
|
||||
public FooterButtonMods()
|
||||
{
|
||||
// must be created in ctor for correct operation of `Current`.
|
||||
modDisplay = new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(0.8f),
|
||||
ExpansionMode = ExpansionMode.AlwaysContracted,
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
SelectedColour = colours.Yellow;
|
||||
DeselectedColour = SelectedColour.Opacity(0.5f);
|
||||
lowMultiplierColour = colours.Green;
|
||||
highMultiplierColour = colours.Red;
|
||||
Text = @"mods";
|
||||
Hotkey = GlobalAction.ToggleModSelection;
|
||||
|
||||
ButtonContentContainer.AddRange(new Drawable[]
|
||||
{
|
||||
modDisplay,
|
||||
MultiplierText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold),
|
||||
},
|
||||
UnrankedBadge = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Colour = colours.Yellow,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Colour = colours.Gray2,
|
||||
Padding = new MarginPadding(5),
|
||||
UseFullGlyphHeight = false,
|
||||
Text = ModSelectOverlayStrings.Unranked.ToLower()
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(mods =>
|
||||
{
|
||||
modSettingChangeTracker?.Dispose();
|
||||
|
||||
updateMultiplierText();
|
||||
|
||||
if (mods.NewValue != null)
|
||||
{
|
||||
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
|
||||
modSettingChangeTracker.SettingChanged += _ => updateMultiplierText();
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void updateMultiplierText() => Schedule(() =>
|
||||
{
|
||||
double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1;
|
||||
MultiplierText.Text = multiplier == 1 ? string.Empty : ModUtils.FormatScoreMultiplier(multiplier);
|
||||
|
||||
if (multiplier > 1)
|
||||
MultiplierText.FadeColour(highMultiplierColour, 200);
|
||||
else if (multiplier < 1)
|
||||
MultiplierText.FadeColour(lowMultiplierColour, 200);
|
||||
else
|
||||
MultiplierText.FadeColour(Color4.White, 200);
|
||||
|
||||
if (Current.Value?.Count > 0)
|
||||
modDisplay.FadeIn();
|
||||
else
|
||||
modDisplay.FadeOut();
|
||||
|
||||
bool anyUnrankedMods = Current.Value?.Any(m => !m.Ranked) == true;
|
||||
UnrankedBadge.FadeTo(anyUnrankedMods ? 1 : 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,22 +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 osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class FooterButtonOptions : FooterButton
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
SelectedColour = colours.Blue;
|
||||
DeselectedColour = SelectedColour.Opacity(0.5f);
|
||||
Text = @"options";
|
||||
Hotkey = GlobalAction.ToggleBeatmapOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class FooterButtonRandom : FooterButton
|
||||
{
|
||||
public Action NextRandom { get; set; }
|
||||
public Action PreviousRandom { get; set; }
|
||||
|
||||
private Container persistentText;
|
||||
private OsuSpriteText randomSpriteText;
|
||||
private OsuSpriteText rewindSpriteText;
|
||||
private bool rewindSearch;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
SelectedColour = colours.Green;
|
||||
DeselectedColour = SelectedColour.Opacity(0.5f);
|
||||
|
||||
TextContainer.Add(persistentText = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AlwaysPresent = true,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
randomSpriteText = new OsuSpriteText
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "random",
|
||||
},
|
||||
rewindSpriteText = new OsuSpriteText
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "rewind",
|
||||
Alpha = 0f,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Action = () =>
|
||||
{
|
||||
if (rewindSearch)
|
||||
{
|
||||
const double fade_time = 500;
|
||||
|
||||
OsuSpriteText fallingRewind;
|
||||
|
||||
TextContainer.Add(fallingRewind = new OsuSpriteText
|
||||
{
|
||||
Alpha = 0,
|
||||
Text = rewindSpriteText.Text,
|
||||
AlwaysPresent = true, // make sure the button is sized large enough to always show this
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
fallingRewind.FadeOutFromOne(fade_time, Easing.In);
|
||||
fallingRewind.MoveTo(Vector2.Zero).MoveTo(new Vector2(0, 10), fade_time, Easing.In);
|
||||
fallingRewind.Expire();
|
||||
|
||||
persistentText.FadeInFromZero(fade_time, Easing.In);
|
||||
|
||||
PreviousRandom.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
NextRandom.Invoke();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
updateText(e);
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
updateText(e);
|
||||
base.OnKeyUp(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
updateText(e);
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// this uses OR to handle rewinding when clicks are triggered by other sources (i.e. right button in OnMouseUp).
|
||||
rewindSearch |= e.ShiftPressed;
|
||||
return base.OnClick(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
rewindSearch = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
base.OnMouseUp(e);
|
||||
|
||||
if (e.Button == MouseButton.Right && IsHovered)
|
||||
{
|
||||
rewindSearch = true;
|
||||
TriggerClick();
|
||||
}
|
||||
|
||||
updateText(e);
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
rewindSearch = e.Action == GlobalAction.SelectPreviousRandom;
|
||||
|
||||
if (e.Action != GlobalAction.SelectNextRandom && e.Action != GlobalAction.SelectPreviousRandom)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!e.Repeat)
|
||||
TriggerClick();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Action == GlobalAction.SelectPreviousRandom)
|
||||
{
|
||||
rewindSearch = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateText(UIEvent e)
|
||||
{
|
||||
bool aboutToRewind = e.ShiftPressed || e.CurrentState.Mouse.IsPressed(MouseButton.Right);
|
||||
|
||||
randomSpriteText.Alpha = aboutToRewind ? 0 : 1;
|
||||
rewindSpriteText.Alpha = aboutToRewind ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class NoResultsPlaceholder : VisibilityContainer
|
||||
{
|
||||
private FilterCriteria? filter;
|
||||
|
||||
private LinkFlowContainer textFlow = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private FirstRunSetupOverlay? firstRunSetupOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
public FilterCriteria Filter
|
||||
{
|
||||
set
|
||||
{
|
||||
if (filter == value)
|
||||
return;
|
||||
|
||||
filter = value;
|
||||
Scheduler.AddOnce(updateText);
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Masking = true;
|
||||
CornerRadius = 10;
|
||||
|
||||
Width = 400;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.Gray2,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Icon = FontAwesome.Regular.SadTear,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Margin = new MarginPadding(10),
|
||||
Size = new Vector2(50),
|
||||
},
|
||||
textFlow = new LinkFlowContainer
|
||||
{
|
||||
Y = 60,
|
||||
Padding = new MarginPadding(10),
|
||||
TextAnchor = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(600, Easing.OutQuint);
|
||||
|
||||
Scheduler.AddOnce(updateText);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(200, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void updateText()
|
||||
{
|
||||
// TODO: Refresh this text when new beatmaps are imported. Right now it won't get up-to-date suggestions.
|
||||
|
||||
// Bounce should play every time the filter criteria is updated.
|
||||
this.ScaleTo(0.9f)
|
||||
.ScaleTo(1f, 1000, Easing.OutElastic);
|
||||
|
||||
textFlow.Clear();
|
||||
|
||||
if (beatmaps.QueryBeatmapSet(s => !s.Protected && !s.DeletePending) == null)
|
||||
{
|
||||
textFlow.AddParagraph("No beatmaps found!");
|
||||
textFlow.AddParagraph(string.Empty);
|
||||
|
||||
textFlow.AddParagraph("- Consider running the \"");
|
||||
textFlow.AddLink(FirstRunSetupOverlayStrings.FirstRunSetupTitle, () => firstRunSetupOverlay?.Show());
|
||||
textFlow.AddText("\" to download or import some beatmaps!");
|
||||
}
|
||||
else
|
||||
{
|
||||
textFlow.AddParagraph("No beatmaps match your filter criteria!");
|
||||
textFlow.AddParagraph(string.Empty);
|
||||
|
||||
if (filter?.UserStarDifficulty.HasFilter == true)
|
||||
{
|
||||
textFlow.AddParagraph("- Try ");
|
||||
textFlow.AddLink("removing", () =>
|
||||
{
|
||||
config.SetValue(OsuSetting.DisplayStarsMinimum, 0.0);
|
||||
config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1);
|
||||
});
|
||||
|
||||
string lowerStar = $"{filter.UserStarDifficulty.Min ?? 0:N1}";
|
||||
string upperStar = filter.UserStarDifficulty.Max == null ? "∞" : $"{filter.UserStarDifficulty.Max:N1}";
|
||||
|
||||
textFlow.AddText($" the {lowerStar} - {upperStar} star difficulty filter.");
|
||||
}
|
||||
|
||||
// TODO: Add realm queries to hint at which ruleset results are available in (and allow clicking to switch).
|
||||
// TODO: Make this message more certain by ensuring the osu! beatmaps exist before suggesting.
|
||||
if (filter?.Ruleset?.OnlineID != 0 && filter?.AllowConvertedBeatmaps == false)
|
||||
{
|
||||
textFlow.AddParagraph("- Try ");
|
||||
textFlow.AddLink("enabling ", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
|
||||
textFlow.AddText("automatic conversion!");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(filter?.SearchText))
|
||||
{
|
||||
textFlow.AddParagraph("- Try ");
|
||||
textFlow.AddLink("searching online", LinkAction.SearchBeatmapSet, filter.SearchText);
|
||||
textFlow.AddText($" for \"{filter.SearchText}\".");
|
||||
}
|
||||
// TODO: add clickable link to reset criteria.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,159 +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 osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Screens.Select.Options
|
||||
{
|
||||
public partial class BeatmapOptionsButton : OsuClickableContainer
|
||||
{
|
||||
private const float width = 130;
|
||||
|
||||
private readonly Box background;
|
||||
private readonly Box flash;
|
||||
private readonly SpriteIcon iconText;
|
||||
private readonly OsuSpriteText firstLine;
|
||||
private readonly OsuSpriteText secondLine;
|
||||
private readonly Container box;
|
||||
|
||||
public Color4 ButtonColour
|
||||
{
|
||||
get => background.Colour;
|
||||
set => background.Colour = value;
|
||||
}
|
||||
|
||||
public IconUsage Icon
|
||||
{
|
||||
get => iconText.Icon;
|
||||
set => iconText.Icon = value;
|
||||
}
|
||||
|
||||
public LocalisableString FirstLineText
|
||||
{
|
||||
get => firstLine.Text;
|
||||
set => firstLine.Text = value;
|
||||
}
|
||||
|
||||
public LocalisableString SecondLineText
|
||||
{
|
||||
get => secondLine.Text;
|
||||
set => secondLine.Text = value;
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
flash.FadeTo(0.1f, 1000, Easing.OutQuint);
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
flash.FadeTo(0, 1000, Easing.OutQuint);
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
flash.ClearTransforms();
|
||||
flash.Alpha = 0.9f;
|
||||
flash.FadeOut(800, Easing.OutExpo);
|
||||
|
||||
return base.OnClick(e);
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
public BeatmapOptionsButton()
|
||||
: base(HoverSampleSet.Button)
|
||||
{
|
||||
Width = width;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
box = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Masking = true,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.2f),
|
||||
Roundness = 5,
|
||||
Radius = 8,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
EdgeSmoothness = new Vector2(1.5f, 0),
|
||||
Colour = Color4.Black,
|
||||
},
|
||||
flash = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
EdgeSmoothness = new Vector2(1.5f, 0),
|
||||
Blending = BlendingParameters.Additive,
|
||||
Colour = Color4.White,
|
||||
Alpha = 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
iconText = new SpriteIcon
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Size = new Vector2(30),
|
||||
Shadow = true,
|
||||
Icon = FontAwesome.Solid.TimesCircle,
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Bottom = 5,
|
||||
},
|
||||
},
|
||||
firstLine = new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold),
|
||||
Text = @"",
|
||||
},
|
||||
secondLine = new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold),
|
||||
Text = @"",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,137 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.Select.Options
|
||||
{
|
||||
public partial class BeatmapOptionsOverlay : OsuFocusedOverlayContainer
|
||||
{
|
||||
private const float transition_duration = 500;
|
||||
private const float x_position = 0.2f;
|
||||
private const float x_movement = 0.8f;
|
||||
|
||||
private const float height = 100;
|
||||
|
||||
private readonly Box holder;
|
||||
private readonly FillFlowContainer<BeatmapOptionsButton> buttonsContainer;
|
||||
|
||||
public override bool BlockScreenWideMouse => false;
|
||||
|
||||
protected override string PopInSampleName => "SongSelect/options-pop-in";
|
||||
protected override string PopOutSampleName => "SongSelect/options-pop-out";
|
||||
|
||||
public BeatmapOptionsOverlay()
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Anchor = Anchor.BottomLeft;
|
||||
Origin = Anchor.BottomLeft;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
holder = new Box
|
||||
{
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.5f,
|
||||
Scale = new Vector2(1, 0),
|
||||
Colour = Color4.Black.Opacity(0.5f),
|
||||
},
|
||||
buttonsContainer = new ReverseChildIDFillFlowContainer<BeatmapOptionsButton>
|
||||
{
|
||||
Height = height,
|
||||
RelativePositionAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// <param name="firstLine">Text in the first line.</param>
|
||||
/// <param name="secondLine">Text in the second line.</param>
|
||||
/// <param name="colour">Colour of the button.</param>
|
||||
/// <param name="icon">Icon of the button.</param>
|
||||
/// <param name="action">Binding the button does.</param>
|
||||
public void AddButton(LocalisableString firstLine, string secondLine, IconUsage icon, Color4 colour, Action action)
|
||||
{
|
||||
var button = new BeatmapOptionsButton
|
||||
{
|
||||
FirstLineText = firstLine,
|
||||
SecondLineText = secondLine,
|
||||
Icon = icon,
|
||||
ButtonColour = colour,
|
||||
Action = () =>
|
||||
{
|
||||
Hide();
|
||||
action?.Invoke();
|
||||
},
|
||||
};
|
||||
|
||||
buttonsContainer.Add(button);
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(transition_duration, Easing.OutQuint);
|
||||
|
||||
if (buttonsContainer.Position.X == 1 || Alpha == 0)
|
||||
buttonsContainer.MoveToX(x_position - x_movement);
|
||||
|
||||
holder.ScaleTo(new Vector2(1, 1), transition_duration / 2, Easing.OutQuint);
|
||||
|
||||
buttonsContainer.MoveToX(x_position, transition_duration, Easing.OutQuint);
|
||||
buttonsContainer.TransformSpacingTo(Vector2.Zero, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
|
||||
holder.ScaleTo(new Vector2(1, 0), transition_duration / 2, Easing.InSine);
|
||||
|
||||
buttonsContainer.MoveToX(x_position + x_movement, transition_duration, Easing.InSine);
|
||||
buttonsContainer.TransformSpacingTo(new Vector2(200f, 0f), transition_duration, Easing.InSine);
|
||||
|
||||
this.FadeOut(transition_duration, Easing.InQuint);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
// don't absorb control as ToolbarRulesetSelector uses control + number to navigate
|
||||
if (e.ControlPressed) return false;
|
||||
|
||||
if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9)
|
||||
{
|
||||
int requested = e.Key - Key.Number1;
|
||||
|
||||
// go reverse as buttonsContainer is a ReverseChildIDFillFlowContainer
|
||||
BeatmapOptionsButton found = buttonsContainer.Children.ElementAtOrDefault((buttonsContainer.Children.Count - 1) - requested);
|
||||
|
||||
if (found != null)
|
||||
{
|
||||
found.TriggerClick();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,150 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class PlayBeatmapDetailArea : BeatmapDetailArea
|
||||
{
|
||||
public readonly BeatmapLeaderboard Leaderboard;
|
||||
|
||||
public override WorkingBeatmap Beatmap
|
||||
{
|
||||
get => base.Beatmap;
|
||||
set
|
||||
{
|
||||
base.Beatmap = value;
|
||||
|
||||
Leaderboard.BeatmapInfo = value is DummyWorkingBeatmap ? null : value?.BeatmapInfo;
|
||||
}
|
||||
}
|
||||
|
||||
private Bindable<BeatmapDetailTab> selectedTab;
|
||||
|
||||
private Bindable<bool> selectedModsFilter;
|
||||
|
||||
public PlayBeatmapDetailArea()
|
||||
{
|
||||
Add(Leaderboard = new BeatmapLeaderboard { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
selectedTab = config.GetBindable<BeatmapDetailTab>(OsuSetting.BeatmapDetailTab);
|
||||
selectedModsFilter = config.GetBindable<bool>(OsuSetting.BeatmapDetailModsFilter);
|
||||
|
||||
selectedTab.BindValueChanged(tab => CurrentTab.Value = getTabItemFromTabType(tab.NewValue), true);
|
||||
CurrentTab.BindValueChanged(tab => selectedTab.Value = getTabTypeFromTabItem(tab.NewValue));
|
||||
|
||||
selectedModsFilter.BindValueChanged(checkbox => CurrentModsFilter.Value = checkbox.NewValue, true);
|
||||
CurrentModsFilter.BindValueChanged(checkbox => selectedModsFilter.Value = checkbox.NewValue);
|
||||
}
|
||||
|
||||
public override void Refresh()
|
||||
{
|
||||
base.Refresh();
|
||||
|
||||
Leaderboard.RefetchScores();
|
||||
}
|
||||
|
||||
protected override void OnTabChanged(BeatmapDetailAreaTabItem tab, bool selectedMods)
|
||||
{
|
||||
base.OnTabChanged(tab, selectedMods);
|
||||
|
||||
Leaderboard.FilterMods = selectedMods;
|
||||
|
||||
switch (tab)
|
||||
{
|
||||
case BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope> leaderboard:
|
||||
Leaderboard.Scope = leaderboard.Scope;
|
||||
Leaderboard.Show();
|
||||
break;
|
||||
|
||||
default:
|
||||
Leaderboard.Hide();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override BeatmapDetailAreaTabItem[] CreateTabItems() => base.CreateTabItems().Concat(new BeatmapDetailAreaTabItem[]
|
||||
{
|
||||
new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local),
|
||||
new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Global),
|
||||
new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Country),
|
||||
new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Friend),
|
||||
new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Team),
|
||||
}).ToArray();
|
||||
|
||||
private BeatmapDetailAreaTabItem getTabItemFromTabType(BeatmapDetailTab type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case BeatmapDetailTab.Details:
|
||||
return new BeatmapDetailAreaDetailTabItem();
|
||||
|
||||
case BeatmapDetailTab.Local:
|
||||
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local);
|
||||
|
||||
case BeatmapDetailTab.Global:
|
||||
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Global);
|
||||
|
||||
case BeatmapDetailTab.Country:
|
||||
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Country);
|
||||
|
||||
case BeatmapDetailTab.Friends:
|
||||
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Friend);
|
||||
|
||||
case BeatmapDetailTab.Team:
|
||||
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Team);
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(type));
|
||||
}
|
||||
}
|
||||
|
||||
private BeatmapDetailTab getTabTypeFromTabItem(BeatmapDetailAreaTabItem item)
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
case BeatmapDetailAreaDetailTabItem:
|
||||
return BeatmapDetailTab.Details;
|
||||
|
||||
case BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope> leaderboardTab:
|
||||
switch (leaderboardTab.Scope)
|
||||
{
|
||||
case BeatmapLeaderboardScope.Local:
|
||||
return BeatmapDetailTab.Local;
|
||||
|
||||
case BeatmapLeaderboardScope.Country:
|
||||
return BeatmapDetailTab.Country;
|
||||
|
||||
case BeatmapLeaderboardScope.Global:
|
||||
return BeatmapDetailTab.Global;
|
||||
|
||||
case BeatmapLeaderboardScope.Friend:
|
||||
return BeatmapDetailTab.Friends;
|
||||
|
||||
case BeatmapLeaderboardScope.Team:
|
||||
return BeatmapDetailTab.Team;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(item));
|
||||
}
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +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 osu.Framework.Allocation;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class SkinDeleteDialog : DeletionDialog
|
||||
{
|
||||
private readonly Skin skin;
|
||||
|
||||
public SkinDeleteDialog(Skin skin)
|
||||
{
|
||||
this.skin = skin;
|
||||
BodyText = skin.SkinInfo.Value.Name;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SkinManager manager)
|
||||
{
|
||||
DangerousAction = () =>
|
||||
{
|
||||
manager.Delete(skin.SkinInfo.Value);
|
||||
manager.CurrentSkinInfo.SetDefault();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,39 +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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class WedgeBackground : Container
|
||||
{
|
||||
public WedgeBackground()
|
||||
{
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(1, 0.5f),
|
||||
Colour = Color4.Black,
|
||||
Shear = new Vector2(0.15f, 0),
|
||||
EdgeSmoothness = new Vector2(2, 0),
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RelativePositionAxes = Axes.Y,
|
||||
Size = new Vector2(1, -0.5f),
|
||||
Position = new Vector2(0, 1),
|
||||
Colour = Color4.Black,
|
||||
Shear = new Vector2(-0.15f, 0),
|
||||
EdgeSmoothness = new Vector2(2, 0),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles mouse interactions required when moving away from the carousel.
|
||||
/// </summary>
|
||||
internal partial class LeftSideInteractionContainer : Container
|
||||
{
|
||||
private readonly Action? resetCarouselPosition;
|
||||
|
||||
private bool mouseContained;
|
||||
|
||||
private InputManager inputManager = null!;
|
||||
|
||||
public LeftSideInteractionContainer(Action resetCarouselPosition)
|
||||
{
|
||||
this.resetCarouselPosition = resetCarouselPosition;
|
||||
}
|
||||
|
||||
// we want to block plain scrolls on the left side so that they don't scroll the carousel,
|
||||
// but also we *don't* want to handle scrolls when they're combined with keyboard modifiers
|
||||
// as those will usually correspond to other interactions like adjusting volume.
|
||||
protected override bool OnScroll(ScrollEvent e) => !e.ControlPressed && !e.AltPressed && !e.ShiftPressed && !e.SuperPressed;
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
inputManager = GetContainingInputManager()!;
|
||||
base.LoadComplete();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// We want to trigger an action whenever the cursor is in the left area of song select.
|
||||
// Other elements in song select handle input, so rather than using `OnHover` let's check the true mouse position.
|
||||
if (Contains(inputManager.CurrentState.Mouse.Position))
|
||||
{
|
||||
if (!mouseContained)
|
||||
{
|
||||
mouseContained = true;
|
||||
resetCarouselPosition?.Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mouseContained = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
/// <summary>
|
||||
/// This screen is intended to house all components introduced in the new song select design to add transitions and examine the overall look.
|
||||
/// This will be gradually built upon and ultimately replace <see cref="Select.SongSelect"/> once everything is in place.
|
||||
/// This will be gradually built upon and ultimately replace <see cref="SongSelect"/> once everything is in place.
|
||||
/// </summary>
|
||||
public abstract partial class SongSelect : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, ISongSelect, IHandlePresentBeatmap, IProvideCursor
|
||||
{
|
||||
@@ -228,7 +228,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
// Pad enough to only reset scroll when well into the left wedge areas.
|
||||
Padding = new MarginPadding { Right = 40 },
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Select.SongSelect.LeftSideInteractionContainer(() => carousel.ScrollToSelection())
|
||||
Child = new LeftSideInteractionContainer(() => carousel.ScrollToSelection())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user