diff --git a/README.md b/README.md index 19aba5a31f..55f2eebec9 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ If the build fails, try to restore nuget packages with `dotnet restore`. On Linux, the environment variable `LD_LIBRARY_PATH` must point to the build directory, located at `osu.Desktop/bin/Debug/$NETCORE_VERSION`. -`$NETCORE_VERSION` is the version of .NET Core SDK. You can have it with `grep TargetFramework osu.Desktop/osu.Desktop.csproj | sed -r 's/.*>(.*)<\/.*/\1/'`. +`$NETCORE_VERSION` is the version of the targeted .NET Core SDK. You can check it by running `grep TargetFramework osu.Desktop/osu.Desktop.csproj | sed -r 's/.*>(.*)<\/.*/\1/'`. For example, you can run osu! with the following command: diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 9cbff8c5d3..a603d96201 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Tests.Visual; @@ -17,8 +16,8 @@ namespace osu.Game.Rulesets.Catch.Tests { } - [BackgroundDependencyLoader] - private void load() + [Test] + public void TestHyperDash() { AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); } diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index d72c334ed3..893c7875fa 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . 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.Bindables; using osu.Framework.Graphics; @@ -73,7 +74,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { case OsuAction.LeftButton: case OsuAction.RightButton: - if (--downCount == 0) + // Todo: Math.Max() is required as a temporary measure to address https://github.com/ppy/osu-framework/issues/2576 + downCount = Math.Max(0, downCount - 1); + + if (downCount == 0) updateExpandedState(); break; } diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 5fc05a4b2f..ad0ed00989 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -108,7 +108,7 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); // ReSharper disable once AccessToModifiedClosure - manager.ItemAdded += (_, __) => Interlocked.Increment(ref itemAddRemoveFireCount); + manager.ItemAdded += _ => Interlocked.Increment(ref itemAddRemoveFireCount); manager.ItemRemoved += _ => Interlocked.Increment(ref itemAddRemoveFireCount); var imported = await LoadOszIntoOsu(osu); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index ac10c77a78..5808a78056 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -137,6 +137,22 @@ namespace osu.Game.Tests.Visual.Gameplay exitAndConfirm(); } + [Test] + public void TestExitViaHoldToExit() + { + AddStep("exit", () => + { + InputManager.MoveMouseTo(Player.HUDOverlay.HoldToQuit.First(c => c is HoldToConfirmContainer)); + InputManager.PressButton(MouseButton.Left); + }); + + confirmPaused(); + + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + + exitAndConfirm(); + } + [Test] public void TestExitFromPause() { diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 0df6605cdd..f24589ed35 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.Menus { typeof(ToolbarButton), typeof(ToolbarRulesetSelector), - typeof(ToolbarRulesetButton), + typeof(ToolbarRulesetTabButton), typeof(ToolbarNotificationButton), }; diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs new file mode 100644 index 0000000000..fe94165777 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Overlays.BeatmapSet; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public class TestSceneBeatmapAvailability : OsuTestScene + { + private readonly BeatmapAvailability container; + + public TestSceneBeatmapAvailability() + { + Add(container = new BeatmapAvailability()); + } + + [Test] + public void TestUndownloadableWithLink() + { + AddStep("set undownloadable beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Availability = new BeatmapSetOnlineAvailability + { + DownloadDisabled = true, + ExternalLink = @"https://osu.ppy.sh", + }, + }, + }); + + visiblityAssert(true); + } + + [Test] + public void TestUndownloadableNoLink() + { + AddStep("set undownloadable beatmapset without link", () => container.BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Availability = new BeatmapSetOnlineAvailability + { + DownloadDisabled = true, + }, + }, + }); + + visiblityAssert(true); + } + + [Test] + public void TestPartsRemovedWithLink() + { + AddStep("set parts-removed beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Availability = new BeatmapSetOnlineAvailability + { + DownloadDisabled = false, + ExternalLink = @"https://osu.ppy.sh", + }, + }, + }); + + visiblityAssert(true); + } + + [Test] + public void TestNormal() + { + AddStep("set normal beatmapset", () => container.BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Availability = new BeatmapSetOnlineAvailability + { + DownloadDisabled = false, + }, + }, + }); + + visiblityAssert(false); + } + + private void visiblityAssert(bool shown) + { + AddAssert($"is container {(shown ? "visible" : "hidden")}", () => container.Alpha == (shown ? 1 : 0)); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index fb2d4efc68..c494f5ef33 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneBeatmapSetOverlay : OsuTestScene { - private readonly BeatmapSetOverlay overlay; + private readonly TestBeatmapSetOverlay overlay; public override IReadOnlyList RequiredTypes => new[] { @@ -39,21 +39,22 @@ namespace osu.Game.Tests.Visual.Online typeof(Info), typeof(PreviewButton), typeof(SuccessRate), + typeof(BeatmapAvailability), }; - private RulesetInfo maniaRuleset; private RulesetInfo taikoRuleset; + private RulesetInfo maniaRuleset; public TestSceneBeatmapSetOverlay() { - Add(overlay = new BeatmapSetOverlay()); + Add(overlay = new TestBeatmapSetOverlay()); } [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - maniaRuleset = rulesets.GetRuleset(3); taikoRuleset = rulesets.GetRuleset(1); + maniaRuleset = rulesets.GetRuleset(3); } [Test] @@ -75,159 +76,53 @@ namespace osu.Game.Tests.Visual.Online { overlay.ShowBeatmapSet(new BeatmapSetInfo { + OnlineBeatmapSetID = 1235, Metadata = new BeatmapMetadata { - Title = @"Lachryma ", - Artist = @"Kaneko Chiharu", - Source = @"SOUND VOLTEX III GRAVITY WARS", - Tags = @"sdvx grace the 5th kac original song contest konami bemani", + Title = @"an awesome beatmap", + Artist = @"naru narusegawa", + Source = @"hinata sou", + Tags = @"test tag tag more tag", Author = new User { - Username = @"Fresh Chicken", - Id = 3984370, + Username = @"BanchoBot", + Id = 3, }, }, OnlineInfo = new BeatmapSetOnlineInfo { - Preview = @"https://b.ppy.sh/preview/415886.mp3", - PlayCount = 681380, - FavouriteCount = 356, - Submitted = new DateTime(2016, 2, 10), - Ranked = new DateTime(2016, 6, 19), - Status = BeatmapSetOnlineStatus.Ranked, - BPM = 236, + Preview = @"https://b.ppy.sh/preview/12345.mp3", + PlayCount = 123, + FavouriteCount = 456, + Submitted = DateTime.Now, + Ranked = DateTime.Now, + BPM = 111, HasVideo = true, - Covers = new BeatmapSetOnlineCovers - { - Cover = @"https://assets.ppy.sh/beatmaps/415886/covers/cover.jpg?1465651778", - }, + HasStoryboard = true, + Covers = new BeatmapSetOnlineCovers(), }, Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() }, Beatmaps = new List { new BeatmapInfo { - StarDifficulty = 1.36, - Version = @"BASIC", + StarDifficulty = 9.99, + Version = @"TEST", Ruleset = maniaRuleset, BaseDifficulty = new BeatmapDifficulty { - CircleSize = 4, - DrainRate = 6.5f, - OverallDifficulty = 6.5f, - ApproachRate = 5, + CircleSize = 1, + DrainRate = 2.3f, + OverallDifficulty = 4.5f, + ApproachRate = 6, }, OnlineInfo = new BeatmapOnlineInfo { - Length = 115000, - CircleCount = 265, - SliderCount = 71, - PlayCount = 47906, - PassCount = 19899, - }, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - }, - new BeatmapInfo - { - StarDifficulty = 2.22, - Version = @"NOVICE", - Ruleset = maniaRuleset, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 4, - DrainRate = 7, - OverallDifficulty = 7, - ApproachRate = 5, - }, - OnlineInfo = new BeatmapOnlineInfo - { - Length = 118000, - CircleCount = 592, - SliderCount = 62, - PlayCount = 162021, - PassCount = 72116, - }, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - }, - new BeatmapInfo - { - StarDifficulty = 3.49, - Version = @"ADVANCED", - Ruleset = maniaRuleset, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 4, - DrainRate = 7.5f, - OverallDifficulty = 7.5f, - ApproachRate = 5, - }, - OnlineInfo = new BeatmapOnlineInfo - { - Length = 118000, - CircleCount = 1042, - SliderCount = 79, - PlayCount = 225178, - PassCount = 73001, - }, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - }, - new BeatmapInfo - { - StarDifficulty = 4.24, - Version = @"EXHAUST", - Ruleset = maniaRuleset, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 4, - DrainRate = 8, - OverallDifficulty = 8, - ApproachRate = 5, - }, - OnlineInfo = new BeatmapOnlineInfo - { - Length = 118000, - CircleCount = 1352, - SliderCount = 69, - PlayCount = 131545, - PassCount = 42703, - }, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - }, - new BeatmapInfo - { - StarDifficulty = 5.26, - Version = @"GRAVITY", - Ruleset = maniaRuleset, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 4, - DrainRate = 8.5f, - OverallDifficulty = 8.5f, - ApproachRate = 5, - }, - OnlineInfo = new BeatmapOnlineInfo - { - Length = 118000, - CircleCount = 1730, - SliderCount = 115, - PlayCount = 117673, - PassCount = 24241, + Length = 456000, + CircleCount = 111, + SliderCount = 12, + PlayCount = 222, + PassCount = 21, }, Metrics = new BeatmapMetrics { @@ -239,162 +134,68 @@ namespace osu.Game.Tests.Visual.Online }); }); - AddStep(@"show second", () => + downloadAssert(true); + } + + [Test] + public void TestAvailability() + { + AddStep(@"show undownloadable", () => { overlay.ShowBeatmapSet(new BeatmapSetInfo { + OnlineBeatmapSetID = 1234, Metadata = new BeatmapMetadata { - Title = @"Soumatou Labyrinth", - Artist = @"Yunomi with Momobako&miko", - Tags = @"mmbk.com yuzu__rinrin charlotte", + Title = @"undownloadable beatmap", + Artist = @"no one", + Source = @"some source", + Tags = @"another test tag tag more test tags", Author = new User { - Username = @"komasy", - Id = 1980256, + Username = @"BanchoBot", + Id = 3, }, }, OnlineInfo = new BeatmapSetOnlineInfo { - Preview = @"https://b.ppy.sh/preview/625493.mp3", - PlayCount = 22996, - FavouriteCount = 58, - Submitted = new DateTime(2016, 6, 11), - Ranked = new DateTime(2016, 7, 12), - Status = BeatmapSetOnlineStatus.Pending, - BPM = 160, - HasVideo = false, - Covers = new BeatmapSetOnlineCovers + Availability = new BeatmapSetOnlineAvailability { - Cover = @"https://assets.ppy.sh/beatmaps/625493/covers/cover.jpg?1499167472", + DownloadDisabled = true, + ExternalLink = "https://osu.ppy.sh", }, + Preview = @"https://b.ppy.sh/preview/1234.mp3", + PlayCount = 123, + FavouriteCount = 456, + Submitted = DateTime.Now, + Ranked = DateTime.Now, + BPM = 111, + HasVideo = true, + HasStoryboard = true, + Covers = new BeatmapSetOnlineCovers(), }, Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() }, Beatmaps = new List { new BeatmapInfo { - StarDifficulty = 1.40, - Version = @"yzrin's Kantan", + StarDifficulty = 5.67, + Version = @"ANOTHER TEST", Ruleset = taikoRuleset, BaseDifficulty = new BeatmapDifficulty { - CircleSize = 2, - DrainRate = 7, - OverallDifficulty = 3, - ApproachRate = 10, + CircleSize = 9, + DrainRate = 8, + OverallDifficulty = 7, + ApproachRate = 6, }, OnlineInfo = new BeatmapOnlineInfo { - Length = 193000, - CircleCount = 262, - SliderCount = 0, - PlayCount = 3952, - PassCount = 1373, - }, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - }, - new BeatmapInfo - { - StarDifficulty = 2.23, - Version = @"Futsuu", - Ruleset = taikoRuleset, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 2, - DrainRate = 6, - OverallDifficulty = 4, - ApproachRate = 10, - }, - OnlineInfo = new BeatmapOnlineInfo - { - Length = 193000, - CircleCount = 464, - SliderCount = 0, - PlayCount = 4833, - PassCount = 920, - }, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - }, - new BeatmapInfo - { - StarDifficulty = 3.19, - Version = @"Muzukashii", - Ruleset = taikoRuleset, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 2, - DrainRate = 6, - OverallDifficulty = 5, - ApproachRate = 10, - }, - OnlineInfo = new BeatmapOnlineInfo - { - Length = 193000, - CircleCount = 712, - SliderCount = 0, - PlayCount = 4405, - PassCount = 854, - }, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - }, - new BeatmapInfo - { - StarDifficulty = 3.97, - Version = @"Charlotte's Oni", - Ruleset = taikoRuleset, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 5, - DrainRate = 6, - OverallDifficulty = 5.5f, - ApproachRate = 10, - }, - OnlineInfo = new BeatmapOnlineInfo - { - Length = 193000, - CircleCount = 943, - SliderCount = 0, - PlayCount = 3950, - PassCount = 693, - }, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - }, - new BeatmapInfo - { - StarDifficulty = 5.08, - Version = @"Labyrinth Oni", - Ruleset = taikoRuleset, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 5, - DrainRate = 5, - OverallDifficulty = 6, - ApproachRate = 10, - }, - OnlineInfo = new BeatmapOnlineInfo - { - Length = 193000, - CircleCount = 1068, - SliderCount = 0, - PlayCount = 5856, - PassCount = 1207, + Length = 123000, + CircleCount = 123, + SliderCount = 45, + PlayCount = 567, + PassCount = 89, }, Metrics = new BeatmapMetrics { @@ -405,6 +206,8 @@ namespace osu.Game.Tests.Visual.Online }, }); }); + + downloadAssert(false); } [Test] @@ -418,5 +221,15 @@ namespace osu.Game.Tests.Visual.Online { AddStep(@"show without reload", overlay.Show); } + + private void downloadAssert(bool shown) + { + AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.DownloadButtonsVisible == shown); + } + + private class TestBeatmapSetOverlay : BeatmapSetOverlay + { + public bool DownloadButtonsVisible => Header.DownloadButtonsVisible; + } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs new file mode 100644 index 0000000000..5a5833feb6 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs @@ -0,0 +1,158 @@ +// Copyright (c) ppy Pty Ltd . 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.Game.Beatmaps; +using osu.Game.Online; +using osu.Game.Overlays.Direct; +using osu.Game.Rulesets.Osu; +using osu.Game.Tests.Resources; +using osuTK; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneDirectDownloadButton : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DownloadButton) + }; + + private TestDownloadButton downloadButton; + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [Test] + public void TestDownloadableBeatmap() + { + createButton(true); + assertEnabled(true); + } + + [Test] + public void TestUndownloadableBeatmap() + { + createButton(false); + assertEnabled(false); + } + + [Test] + public void TestDownloadState() + { + AddUntilStep("ensure manager loaded", () => beatmaps != null); + ensureSoleilyRemoved(); + createButtonWithBeatmap(createSoleily()); + AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded); + AddStep("import soleily", () => beatmaps.Import(new[] { TestResources.GetTestBeatmapForImport() })); + AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineBeatmapSetID == 241526)); + createButtonWithBeatmap(createSoleily()); + AddAssert("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable); + ensureSoleilyRemoved(); + AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded); + } + + private void ensureSoleilyRemoved() + { + AddStep("remove soleily", () => + { + var beatmap = beatmaps.QueryBeatmapSet(b => b.OnlineBeatmapSetID == 241526); + + if (beatmap != null) beatmaps.Delete(beatmap); + }); + } + + private void assertEnabled(bool enabled) + { + AddAssert($"button {(enabled ? "enabled" : "disabled")}", () => downloadButton.DownloadEnabled == enabled); + } + + private BeatmapSetInfo createSoleily() + { + return new BeatmapSetInfo + { + ID = 1, + OnlineBeatmapSetID = 241526, + OnlineInfo = new BeatmapSetOnlineInfo + { + Availability = new BeatmapSetOnlineAvailability + { + DownloadDisabled = false, + ExternalLink = string.Empty, + }, + }, + }; + } + + private void createButtonWithBeatmap(BeatmapSetInfo beatmap) + { + AddStep("create button", () => + { + Child = downloadButton = new TestDownloadButton(beatmap) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(75, 50), + }; + }); + } + + private void createButton(bool downloadable) + { + AddStep("create button", () => + { + Child = downloadButton = new TestDownloadButton(downloadable ? getDownloadableBeatmapSet() : getUndownloadableBeatmapSet()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(75, 50), + }; + }); + } + + private BeatmapSetInfo getDownloadableBeatmapSet() + { + var normal = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo).BeatmapSetInfo; + normal.OnlineInfo.HasVideo = true; + normal.OnlineInfo.HasStoryboard = true; + + return normal; + } + + private BeatmapSetInfo getUndownloadableBeatmapSet() + { + var beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo).BeatmapSetInfo; + beatmap.Metadata.Artist = "test"; + beatmap.Metadata.Title = "undownloadable"; + beatmap.Metadata.AuthorString = "test"; + + beatmap.OnlineInfo.HasVideo = true; + beatmap.OnlineInfo.HasStoryboard = true; + + beatmap.OnlineInfo.Availability = new BeatmapSetOnlineAvailability + { + DownloadDisabled = true, + ExternalLink = "http://osu.ppy.sh", + }; + + return beatmap; + } + + private class TestDownloadButton : DownloadButton + { + public new bool DownloadEnabled => base.DownloadEnabled; + + public DownloadState DownloadState => State.Value; + + public TestDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false) + : base(beatmapSet, noVideo) + { + } + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs index 8b67892fbb..53dbaeddda 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs @@ -6,8 +6,11 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; using osu.Game.Overlays.Direct; +using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Online @@ -21,25 +24,74 @@ namespace osu.Game.Tests.Visual.Online typeof(IconPill) }; + private BeatmapSetInfo getUndownloadableBeatmapSet(RulesetInfo ruleset) => new BeatmapSetInfo + { + OnlineBeatmapSetID = 123, + Metadata = new BeatmapMetadata + { + Title = "undownloadable beatmap", + Artist = "test", + Source = "more tests", + Author = new User + { + Username = "BanchoBot", + Id = 3, + }, + }, + OnlineInfo = new BeatmapSetOnlineInfo + { + Availability = new BeatmapSetOnlineAvailability + { + DownloadDisabled = true, + }, + Preview = @"https://b.ppy.sh/preview/12345.mp3", + PlayCount = 123, + FavouriteCount = 456, + BPM = 111, + HasVideo = true, + HasStoryboard = true, + Covers = new BeatmapSetOnlineCovers(), + }, + Beatmaps = new List + { + new BeatmapInfo + { + Ruleset = ruleset, + Version = "Test", + StarDifficulty = 6.42, + } + } + }; + [BackgroundDependencyLoader] private void load() { - var beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - beatmap.BeatmapSetInfo.OnlineInfo.HasVideo = true; - beatmap.BeatmapSetInfo.OnlineInfo.HasStoryboard = true; + var ruleset = new OsuRuleset().RulesetInfo; - Child = new FillFlowContainer + var normal = CreateWorkingBeatmap(ruleset).BeatmapSetInfo; + normal.OnlineInfo.HasVideo = true; + normal.OnlineInfo.HasStoryboard = true; + + var undownloadable = getUndownloadableBeatmapSet(ruleset); + + Child = new BasicScrollContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Padding = new MarginPadding(20), - Spacing = new Vector2(0, 20), - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer { - new DirectGridPanel(beatmap.BeatmapSetInfo), - new DirectListPanel(beatmap.BeatmapSetInfo) - } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(20), + Spacing = new Vector2(0, 20), + Children = new Drawable[] + { + new DirectGridPanel(normal), + new DirectListPanel(normal), + new DirectGridPanel(undownloadable), + new DirectListPanel(undownloadable), + }, + }, }; } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs new file mode 100644 index 0000000000..c344cb9598 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Overlays.Profile.Header.Components; +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneProfileRulesetSelector : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ProfileRulesetSelector), + typeof(ProfileRulesetTabItem), + }; + + public TestSceneProfileRulesetSelector() + { + ProfileRulesetSelector selector; + + Child = selector = new ProfileRulesetSelector + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + + AddStep("set osu! as default", () => selector.SetDefaultRuleset(new OsuRuleset().RulesetInfo)); + AddStep("set mania as default", () => selector.SetDefaultRuleset(new ManiaRuleset().RulesetInfo)); + AddStep("set taiko as default", () => selector.SetDefaultRuleset(new TaikoRuleset().RulesetInfo)); + AddStep("set catch as default", () => selector.SetDefaultRuleset(new CatchRuleset().RulesetInfo)); + AddStep("select default ruleset", selector.SelectDefaultRuleset); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs new file mode 100644 index 0000000000..867b3130c9 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneBackButton : OsuTestScene + { + private readonly BackButton button; + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(TwoLayerButton) + }; + + public TestSceneBackButton() + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300), + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.SlateGray + }, + button = new BackButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Action = () => button.Hide(), + } + } + }; + + AddStep("show button", () => button.Show()); + AddStep("hide button", () => button.Hide()); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneToolbarRulesetSelector.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneToolbarRulesetSelector.cs new file mode 100644 index 0000000000..0da256855a --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneToolbarRulesetSelector.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays.Toolbar; +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.MathUtils; +using osu.Game.Rulesets; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneToolbarRulesetSelector : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ToolbarRulesetSelector), + typeof(ToolbarRulesetTabButton), + }; + + [Resolved] + private RulesetStore rulesets { get; set; } + + [Test] + public void TestDisplay() + { + ToolbarRulesetSelector selector = null; + + AddStep("create selector", () => + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.X, + Height = Toolbar.HEIGHT, + Child = selector = new ToolbarRulesetSelector() + }; + }); + + AddStep("Select random", () => + { + selector.Current.Value = selector.Items.ElementAt(RNG.Next(selector.Items.Count())); + }); + AddStep("Toggle disabled state", () => selector.Current.Disabled = !selector.Current.Disabled); + } + + [Test] + public void TestNonFirstRulesetInitialState() + { + TestSelector selector = null; + + AddStep("create selector", () => + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.X, + Height = Toolbar.HEIGHT, + Child = selector = new TestSelector() + }; + + selector.Current.Value = rulesets.GetRuleset(2); + }); + + AddAssert("mode line has moved", () => selector.ModeButtonLine.DrawPosition.X > 0); + } + + private class TestSelector : ToolbarRulesetSelector + { + public new Drawable ModeButtonLine => base.ModeButtonLine; + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneTwoLayerButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneTwoLayerButton.cs index b9ed1a71cc..849577186d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneTwoLayerButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneTwoLayerButton.cs @@ -1,17 +1,26 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.ComponentModel; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface { - [Description("mostly back button")] public class TestSceneTwoLayerButton : OsuTestScene { public TestSceneTwoLayerButton() { - Add(new BackButton()); + Add(new TwoLayerButton + { + Position = new Vector2(100), + Text = "button", + Icon = FontAwesome.Solid.Check, + BackgroundColour = Color4.SlateGray, + HoverColour = Color4.SlateGray.Darken(0.2f) + }); } } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index dd7b3d77bf..fe8fef3e07 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -21,7 +21,6 @@ using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.API.Requests; -using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; namespace osu.Game.Beatmaps @@ -29,7 +28,7 @@ namespace osu.Game.Beatmaps /// /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// - public partial class BeatmapManager : ArchiveModelManager + public partial class BeatmapManager : DownloadableArchiveModelManager { /// /// Fired when a single difficulty has been hidden. @@ -41,16 +40,6 @@ namespace osu.Game.Beatmaps /// public event Action BeatmapRestored; - /// - /// Fired when a beatmap download begins. - /// - public event Action BeatmapDownloadBegan; - - /// - /// Fired when a beatmap download is interrupted, due to user cancellation or other failures. - /// - public event Action BeatmapDownloadFailed; - /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. /// @@ -66,22 +55,17 @@ namespace osu.Game.Beatmaps private readonly BeatmapStore beatmaps; - private readonly IAPIProvider api; - private readonly AudioManager audioManager; private readonly GameHost host; - private readonly List currentDownloads = new List(); - private readonly BeatmapUpdateQueue updateQueue; public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null, WorkingBeatmap defaultBeatmap = null) - : base(storage, contextFactory, new BeatmapStore(contextFactory), host) + : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) { this.rulesets = rulesets; - this.api = api; this.audioManager = audioManager; this.host = host; @@ -94,6 +78,9 @@ namespace osu.Game.Beatmaps updateQueue = new BeatmapUpdateQueue(api); } + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => + new DownloadBeatmapSetRequest(set, minimiseDownloadSize); + protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) { if (archive != null) @@ -158,86 +145,7 @@ namespace osu.Game.Beatmaps void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null); } - /// - /// Downloads a beatmap. - /// This will post notifications tracking progress. - /// - /// The to be downloaded. - /// Whether the beatmap should be downloaded without video. Defaults to false. - /// Downloading can happen - public bool Download(BeatmapSetInfo beatmapSetInfo, bool noVideo = false) - { - var existing = GetExistingDownload(beatmapSetInfo); - - if (existing != null || api == null) return false; - - var downloadNotification = new DownloadNotification - { - Text = $"Downloading {beatmapSetInfo}", - }; - - var request = new DownloadBeatmapSetRequest(beatmapSetInfo, noVideo); - - request.DownloadProgressed += progress => - { - downloadNotification.State = ProgressNotificationState.Active; - downloadNotification.Progress = progress; - }; - - request.Success += filename => - { - Task.Factory.StartNew(async () => - { - // This gets scheduled back to the update thread, but we want the import to run in the background. - await Import(downloadNotification, filename); - currentDownloads.Remove(request); - }, TaskCreationOptions.LongRunning); - }; - - request.Failure += error => - { - BeatmapDownloadFailed?.Invoke(request); - - if (error is OperationCanceledException) return; - - downloadNotification.State = ProgressNotificationState.Cancelled; - Logger.Error(error, "Beatmap download failed!"); - currentDownloads.Remove(request); - }; - - downloadNotification.CancelRequested += () => - { - request.Cancel(); - currentDownloads.Remove(request); - downloadNotification.State = ProgressNotificationState.Cancelled; - return true; - }; - - currentDownloads.Add(request); - PostNotification?.Invoke(downloadNotification); - - // don't run in the main api queue as this is a long-running task. - Task.Factory.StartNew(() => - { - try - { - request.Perform(api); - } - catch - { - // no need to handle here as exceptions will filter down to request.Failure above. - } - }, TaskCreationOptions.LongRunning); - BeatmapDownloadBegan?.Invoke(request); - return true; - } - - /// - /// Get an existing download request if it exists. - /// - /// The whose download request is wanted. - /// The object if it exists, or null. - public DownloadBeatmapSetRequest GetExistingDownload(BeatmapSetInfo beatmap) => currentDownloads.Find(d => d.BeatmapSet.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID); + protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) => items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID); /// /// Delete a beatmap difficulty. @@ -406,22 +314,6 @@ namespace osu.Game.Beatmaps protected override Track GetTrack() => null; } - private class DownloadNotification : ProgressNotification - { - public override bool IsImportant => false; - - protected override Notification CreateCompletionNotification() => new SilencedProgressCompletionNotification - { - Activated = CompletionClickAction, - Text = CompletionText - }; - - private class SilencedProgressCompletionNotification : ProgressCompletionNotification - { - public override bool IsImportant => false; - } - } - private class BeatmapUpdateQueue { private readonly IAPIProvider api; diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 4b1bddbf0d..5657b8fb8a 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -41,7 +41,7 @@ namespace osu.Game.Beatmaps } } - private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath; + private string getPathForFile(string filename) => BeatmapSetInfo.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase))?.FileInfo.StoragePath; private TextureStore textureStore; diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index c09119ab14..524ed0ed56 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -9,7 +9,7 @@ using osu.Game.Database; namespace osu.Game.Beatmaps { - public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete + public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable { public int ID { get; set; } @@ -49,5 +49,7 @@ namespace osu.Game.Beatmaps public override string ToString() => Metadata?.ToString() ?? base.ToString(); public bool Protected { get; set; } + + public bool Equals(BeatmapSetInfo other) => OnlineBeatmapSetID == other?.OnlineBeatmapSetID; } } diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs index 0ccc9a924c..ea3f0b61b9 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs @@ -65,6 +65,11 @@ namespace osu.Game.Beatmaps /// The amount of people who have favourited this beatmap set. /// public int FavouriteCount { get; set; } + + /// + /// The availability of this beatmap set. + /// + public BeatmapSetOnlineAvailability Availability { get; set; } } public class BeatmapSetOnlineCovers @@ -84,4 +89,13 @@ namespace osu.Game.Beatmaps [JsonProperty(@"list@2x")] public string List { get; set; } } + + public class BeatmapSetOnlineAvailability + { + [JsonProperty(@"download_disabled")] + public bool DownloadDisabled { get; set; } + + [JsonProperty(@"more_information")] + public string ExternalLink { get; set; } + } } diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs index c7c4c1fb1e..16eecb7198 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs @@ -2,9 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osuTK.Graphics; +using osu.Game.Graphics; namespace osu.Game.Beatmaps.Drawables { @@ -49,7 +50,7 @@ namespace osu.Game.Beatmaps.Drawables Child = new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, + Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.2f), OsuColour.Gray(0.1f)), }; } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 1c8e722589..01455e7d50 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -31,12 +31,10 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ArchiveModelManager, ICanAcceptFiles + public abstract class ArchiveModelManager : ArchiveModelManager, ICanAcceptFiles, IModelManager where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : INamedFileInfo, new() { - public delegate void ItemAddedDelegate(TModel model, bool existing); - /// /// Set an endpoint for notifications to be posted to. /// @@ -46,7 +44,7 @@ namespace osu.Game.Database /// Fired when a new becomes available in the database. /// This is not guaranteed to run on the update thread. /// - public event ItemAddedDelegate ItemAdded; + public event Action ItemAdded; /// /// Fired when a is removed from the database. @@ -67,56 +65,12 @@ namespace osu.Game.Database // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ArchiveImportIPCChannel ipc; - private readonly List queuedEvents = new List(); - - /// - /// Allows delaying of outwards events until an operation is confirmed (at a database level). - /// - private bool delayingEvents; - - /// - /// Begin delaying outwards events. - /// - private void delayEvents() => delayingEvents = true; - - /// - /// Flush delayed events and disable delaying. - /// - /// Whether the flushed events should be performed. - private void flushEvents(bool perform) - { - Action[] events; - - lock (queuedEvents) - { - events = queuedEvents.ToArray(); - queuedEvents.Clear(); - } - - if (perform) - { - foreach (var a in events) - a.Invoke(); - } - - delayingEvents = false; - } - - private void handleEvent(Action a) - { - if (delayingEvents) - lock (queuedEvents) - queuedEvents.Add(a); - else - a.Invoke(); - } - protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStoreWithFileIncludes modelStore, IIpcHost importHost = null) { ContextFactory = contextFactory; ModelStore = modelStore; - ModelStore.ItemAdded += item => handleEvent(() => ItemAdded?.Invoke(item, false)); + ModelStore.ItemAdded += item => handleEvent(() => ItemAdded?.Invoke(item)); ModelStore.ItemRemoved += s => handleEvent(() => ItemRemoved?.Invoke(s)); Files = new FileStore(contextFactory, storage); @@ -345,8 +299,6 @@ namespace osu.Game.Database { Undelete(existing); LogForModel(item, $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); - handleEvent(() => ItemAdded?.Invoke(existing, true)); - // existing item will be used; rollback new import and exit early. rollback(); flushEvents(true); @@ -632,6 +584,54 @@ namespace osu.Game.Database throw new InvalidFormatException($"{path} is not a valid archive"); } + + #region Event handling / delaying + + private readonly List queuedEvents = new List(); + + /// + /// Allows delaying of outwards events until an operation is confirmed (at a database level). + /// + private bool delayingEvents; + + /// + /// Begin delaying outwards events. + /// + private void delayEvents() => delayingEvents = true; + + /// + /// Flush delayed events and disable delaying. + /// + /// Whether the flushed events should be performed. + private void flushEvents(bool perform) + { + Action[] events; + + lock (queuedEvents) + { + events = queuedEvents.ToArray(); + queuedEvents.Clear(); + } + + if (perform) + { + foreach (var a in events) + a.Invoke(); + } + + delayingEvents = false; + } + + private void handleEvent(Action a) + { + if (delayingEvents) + lock (queuedEvents) + queuedEvents.Add(a); + else + a.Invoke(); + } + + #endregion } public abstract class ArchiveModelManager diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/DownloadableArchiveModelManager.cs new file mode 100644 index 0000000000..78c0837ce9 --- /dev/null +++ b/osu.Game/Database/DownloadableArchiveModelManager.cs @@ -0,0 +1,143 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Humanizer; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Online.API; +using osu.Game.Overlays.Notifications; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace osu.Game.Database +{ + /// + /// An that has the ability to download models using an and + /// import them into the store. + /// + /// The model type. + /// The associated file join type. + public abstract class DownloadableArchiveModelManager : ArchiveModelManager, IModelDownloader + where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable + where TFileModel : INamedFileInfo, new() + { + public event Action> DownloadBegan; + + public event Action> DownloadFailed; + + private readonly IAPIProvider api; + + private readonly List> currentDownloads = new List>(); + + private readonly MutableDatabaseBackedStoreWithFileIncludes modelStore; + + protected DownloadableArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, IAPIProvider api, MutableDatabaseBackedStoreWithFileIncludes modelStore, IIpcHost importHost = null) + : base(storage, contextFactory, modelStore, importHost) + { + this.api = api; + this.modelStore = modelStore; + } + + /// + /// Creates the download request for this . + /// + /// The to be downloaded. + /// Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle. + /// The request object. + protected abstract ArchiveDownloadRequest CreateDownloadRequest(TModel model, bool minimiseDownloadSize); + + /// + /// Begin a download for the requested . + /// + /// The to be downloaded. + /// Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle. + /// Whether the download was started. + public bool Download(TModel model, bool minimiseDownloadSize = false) + { + if (!canDownload(model)) return false; + + var request = CreateDownloadRequest(model, minimiseDownloadSize); + + DownloadNotification notification = new DownloadNotification + { + Text = $"Downloading {request.Model}", + }; + + request.DownloadProgressed += progress => + { + notification.State = ProgressNotificationState.Active; + notification.Progress = progress; + }; + + request.Success += filename => + { + Task.Factory.StartNew(async () => + { + // This gets scheduled back to the update thread, but we want the import to run in the background. + await Import(notification, filename); + currentDownloads.Remove(request); + }, TaskCreationOptions.LongRunning); + }; + + request.Failure += error => + { + DownloadFailed?.Invoke(request); + + if (error is OperationCanceledException) return; + + notification.State = ProgressNotificationState.Cancelled; + Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!"); + currentDownloads.Remove(request); + }; + + notification.CancelRequested += () => + { + request.Cancel(); + currentDownloads.Remove(request); + notification.State = ProgressNotificationState.Cancelled; + return true; + }; + + currentDownloads.Add(request); + PostNotification?.Invoke(notification); + + Task.Factory.StartNew(() => request.Perform(api), TaskCreationOptions.LongRunning); + + DownloadBegan?.Invoke(request); + + return true; + } + + public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending)); + + /// + /// Performs implementation specific comparisons to determine whether a given model is present in the local store. + /// + /// The whose existence needs to be checked. + /// The usable items present in the store. + /// Whether the exists. + protected abstract bool CheckLocalAvailability(TModel model, IQueryable items); + + public ArchiveDownloadRequest GetExistingDownload(TModel model) => currentDownloads.Find(r => r.Model.Equals(model)); + + private bool canDownload(TModel model) => GetExistingDownload(model) == null && api != null; + + private class DownloadNotification : ProgressNotification + { + public override bool IsImportant => false; + + protected override Notification CreateCompletionNotification() => new SilencedProgressCompletionNotification + { + Activated = CompletionClickAction, + Text = CompletionText + }; + + private class SilencedProgressCompletionNotification : ProgressCompletionNotification + { + public override bool IsImportant => false; + } + } + } +} diff --git a/osu.Game/Database/IModelDownloader.cs b/osu.Game/Database/IModelDownloader.cs new file mode 100644 index 0000000000..f6f4b0aa42 --- /dev/null +++ b/osu.Game/Database/IModelDownloader.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API; +using System; + +namespace osu.Game.Database +{ + /// + /// Represents a that can download new models from an external source. + /// + /// The model type. + public interface IModelDownloader : IModelManager + where TModel : class + { + /// + /// Fired when a download begins. + /// + event Action> DownloadBegan; + + /// + /// Fired when a download is interrupted, either due to user cancellation or failure. + /// + event Action> DownloadFailed; + + /// + /// Checks whether a given is already available in the local store. + /// + /// The whose existence needs to be checked. + /// Whether the exists. + bool IsAvailableLocally(TModel model); + + /// + /// Begin a download for the requested . + /// + /// The to be downloaded. + /// Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle.. + /// Whether the download was started. + bool Download(TModel model, bool minimiseDownloadSize); + + /// + /// Gets an existing download request if it exists. + /// + /// The whose request is wanted. + /// The object if it exists, otherwise null. + ArchiveDownloadRequest GetExistingDownload(TModel model); + } +} diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs new file mode 100644 index 0000000000..884814cb38 --- /dev/null +++ b/osu.Game/Database/IModelManager.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Database +{ + /// + /// Represents a model manager that publishes events when s are added or removed. + /// + /// The model type. + public interface IModelManager + where TModel : class + { + event Action ItemAdded; + + event Action ItemRemoved; + } +} diff --git a/osu.Game/Graphics/UserInterface/BackButton.cs b/osu.Game/Graphics/UserInterface/BackButton.cs index 10e8227f16..48bf0848ae 100644 --- a/osu.Game/Graphics/UserInterface/BackButton.cs +++ b/osu.Game/Graphics/UserInterface/BackButton.cs @@ -1,26 +1,65 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface { - public class BackButton : TwoLayerButton + public class BackButton : VisibilityContainer, IKeyBindingHandler { + public Action Action; + + private readonly TwoLayerButton button; + public BackButton() { - Text = @"back"; - Icon = OsuIcon.LeftCircle; - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomLeft; + Size = TwoLayerButton.SIZE_EXTENDED; + + Child = button = new TwoLayerButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = @"back", + Icon = OsuIcon.LeftCircle, + Action = () => Action?.Invoke() + }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - BackgroundColour = colours.Pink; - HoverColour = colours.PinkDark; + button.BackgroundColour = colours.Pink; + button.HoverColour = colours.PinkDark; + } + + public bool OnPressed(GlobalAction action) + { + if (action == GlobalAction.Back) + { + Action?.Invoke(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => action == GlobalAction.Back; + + protected override void PopIn() + { + button.MoveToX(0, 400, Easing.OutQuint); + button.FadeIn(150, Easing.OutQuint); + } + + protected override void PopOut() + { + button.MoveToX(-TwoLayerButton.SIZE_EXTENDED.X / 2, 400, Easing.OutQuint); + button.FadeOut(400, Easing.OutQuint); } } } diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 2aa4734476..fc3a7229fa 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -98,10 +98,10 @@ namespace osu.Game.Graphics.UserInterface protected override Menu CreateSubMenu() => new OsuMenu(Direction.Vertical); - protected override ScrollContainer CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction); - protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuDropdownMenuItem(item) { AccentColour = accentColour }; + protected override ScrollContainer CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction); + #region DrawableOsuDropdownMenuItem public class DrawableOsuDropdownMenuItem : DrawableDropdownMenuItem, IHasAccentColour @@ -252,8 +252,8 @@ namespace osu.Game.Graphics.UserInterface Icon = FontAwesome.Solid.ChevronDown, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Margin = new MarginPadding { Right = 4 }, - Size = new Vector2(20), + Margin = new MarginPadding { Right = 5 }, + Size = new Vector2(12), }, }; diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 74ca809395..c4c6950eb1 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -45,10 +45,10 @@ namespace osu.Game.Graphics.UserInterface } } - protected override ScrollContainer CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction); - protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableOsuMenuItem(item); + protected override ScrollContainer CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction); + protected override Menu CreateSubMenu() => new OsuMenu(Direction.Vertical) { Anchor = Direction == Direction.Horizontal ? Anchor.BottomLeft : Anchor.TopRight diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index 36a9aca412..c74d00cd1e 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs @@ -165,7 +165,8 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { this.ResizeTo(SIZE_EXTENDED, transform_time, Easing.OutElastic); - IconLayer.FadeColour(HoverColour, transform_time, Easing.OutElastic); + + IconLayer.FadeColour(HoverColour, transform_time / 2f, Easing.OutQuint); bouncingIcon.ScaleTo(1.1f, transform_time, Easing.OutElastic); @@ -174,16 +175,13 @@ namespace osu.Game.Graphics.UserInterface protected override void OnHoverLost(HoverLostEvent e) { - this.ResizeTo(SIZE_RETRACTED, transform_time, Easing.OutElastic); - IconLayer.FadeColour(TextLayer.Colour, transform_time, Easing.OutElastic); + this.ResizeTo(SIZE_RETRACTED, transform_time, Easing.Out); + IconLayer.FadeColour(TextLayer.Colour, transform_time, Easing.Out); - bouncingIcon.ScaleTo(1, transform_time, Easing.OutElastic); + bouncingIcon.ScaleTo(1, transform_time, Easing.Out); } - protected override bool OnMouseDown(MouseDownEvent e) - { - return true; - } + protected override bool OnMouseDown(MouseDownEvent e) => true; protected override bool OnClick(ClickEvent e) { diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 904aa9c8c0..14d356f889 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -95,9 +95,6 @@ namespace osu.Game.Input.Bindings [Description("Quick retry (hold)")] QuickRetry, - [Description("Quick exit (Hold)")] - QuickExit, - [Description("Take screenshot")] TakeScreenshot, @@ -115,5 +112,8 @@ namespace osu.Game.Input.Bindings [Description("Select")] Select, + + [Description("Quick exit (Hold)")] + QuickExit, } } diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 96f3b85272..e8eff5a3a9 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -88,7 +88,12 @@ namespace osu.Game.Online.API if (checkAndScheduleFailure()) return; - API.Schedule(delegate { Success?.Invoke(); }); + API.Schedule(delegate + { + if (cancelled) return; + + Success?.Invoke(); + }); } public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled")); diff --git a/osu.Game/Online/API/ArchiveDownloadRequest.cs b/osu.Game/Online/API/ArchiveDownloadRequest.cs new file mode 100644 index 0000000000..f1966aeb2b --- /dev/null +++ b/osu.Game/Online/API/ArchiveDownloadRequest.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Online.API +{ + public abstract class ArchiveDownloadRequest : APIDownloadRequest + where TModel : class + { + public readonly TModel Model; + + public float Progress; + + public event Action DownloadProgressed; + + protected ArchiveDownloadRequest(TModel model) + { + Model = model; + + Progressed += (current, total) => DownloadProgressed?.Invoke(Progress = (float)current / total); + } + } +} diff --git a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs index 26e8acc2fc..707c59436d 100644 --- a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs @@ -2,28 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Beatmaps; -using System; namespace osu.Game.Online.API.Requests { - public class DownloadBeatmapSetRequest : APIDownloadRequest + public class DownloadBeatmapSetRequest : ArchiveDownloadRequest { - public readonly BeatmapSetInfo BeatmapSet; - - public float Progress; - - public event Action DownloadProgressed; - private readonly bool noVideo; public DownloadBeatmapSetRequest(BeatmapSetInfo set, bool noVideo) + : base(set) { this.noVideo = noVideo; - BeatmapSet = set; - - Progressed += (current, total) => DownloadProgressed?.Invoke(Progress = (float)current / total); } - protected override string Target => $@"beatmapsets/{BeatmapSet.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}"; + protected override string Target => $@"beatmapsets/{Model.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}"; } } diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 00e08633dd..200a705500 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -63,6 +63,9 @@ namespace osu.Game.Online.API.Requests.Responses set => Author.Id = value; } + [JsonProperty(@"availability")] + private BeatmapSetOnlineAvailability availability { get; set; } + [JsonProperty(@"beatmaps")] private IEnumerable beatmaps { get; set; } @@ -87,6 +90,7 @@ namespace osu.Game.Online.API.Requests.Responses Submitted = submitted, Ranked = ranked, LastUpdated = lastUpdated, + Availability = availability, }, Beatmaps = beatmaps?.Select(b => b.ToBeatmap(rulesets)).ToList(), }; diff --git a/osu.Game/Overlays/Direct/DownloadState.cs b/osu.Game/Online/DownloadState.cs similarity index 89% rename from osu.Game/Overlays/Direct/DownloadState.cs rename to osu.Game/Online/DownloadState.cs index a301c6a3bd..72efbc286e 100644 --- a/osu.Game/Overlays/Direct/DownloadState.cs +++ b/osu.Game/Online/DownloadState.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -namespace osu.Game.Overlays.Direct +namespace osu.Game.Online { public enum DownloadState { diff --git a/osu.Game/Overlays/Direct/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs similarity index 60% rename from osu.Game/Overlays/Direct/DownloadTrackingComposite.cs rename to osu.Game/Online/DownloadTrackingComposite.cs index 37f13aefc8..5eb2bb74bb 100644 --- a/osu.Game/Overlays/Direct/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -2,23 +2,24 @@ // 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.Containers; -using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests; +using osu.Game.Database; +using osu.Game.Online.API; -namespace osu.Game.Overlays.Direct +namespace osu.Game.Online { /// /// A component which tracks a beatmap through potential download/import/deletion. /// - public abstract class DownloadTrackingComposite : CompositeDrawable + public abstract class DownloadTrackingComposite : CompositeDrawable + where TModel : class, IEquatable + where TModelManager : class, IModelDownloader { - public readonly Bindable BeatmapSet = new Bindable(); + protected readonly Bindable Model = new Bindable(); - private BeatmapManager beatmaps; + private TModelManager manager; /// /// Holds the current download state of the beatmap, whether is has already been downloaded, is in progress, or is not downloaded. @@ -27,58 +28,39 @@ namespace osu.Game.Overlays.Direct protected readonly Bindable Progress = new Bindable(); - protected DownloadTrackingComposite(BeatmapSetInfo beatmapSet = null) + protected DownloadTrackingComposite(TModel model = null) { - BeatmapSet.Value = beatmapSet; + Model.Value = model; } [BackgroundDependencyLoader(true)] - private void load(BeatmapManager beatmaps) + private void load(TModelManager manager) { - this.beatmaps = beatmaps; + this.manager = manager; - BeatmapSet.BindValueChanged(setInfo => + Model.BindValueChanged(modelInfo => { - if (setInfo.NewValue == null) + if (modelInfo.NewValue == null) attachDownload(null); - else if (beatmaps.GetAllUsableBeatmapSetsEnumerable().Any(s => s.OnlineBeatmapSetID == setInfo.NewValue.OnlineBeatmapSetID)) + else if (manager.IsAvailableLocally(modelInfo.NewValue)) State.Value = DownloadState.LocallyAvailable; else - attachDownload(beatmaps.GetExistingDownload(setInfo.NewValue)); + attachDownload(manager.GetExistingDownload(modelInfo.NewValue)); }, true); - beatmaps.BeatmapDownloadBegan += download => + manager.DownloadBegan += download => { - if (download.BeatmapSet.OnlineBeatmapSetID == BeatmapSet.Value?.OnlineBeatmapSetID) + if (download.Model.Equals(Model.Value)) attachDownload(download); }; - beatmaps.ItemAdded += setAdded; - beatmaps.ItemRemoved += setRemoved; + manager.ItemAdded += itemAdded; + manager.ItemRemoved += itemRemoved; } - #region Disposal + private ArchiveDownloadRequest attachedRequest; - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (beatmaps != null) - { - beatmaps.BeatmapDownloadBegan -= attachDownload; - beatmaps.ItemAdded -= setAdded; - } - - State.UnbindAll(); - - attachDownload(null); - } - - #endregion - - private DownloadBeatmapSetRequest attachedRequest; - - private void attachDownload(DownloadBeatmapSetRequest request) + private void attachDownload(ArchiveDownloadRequest request) { if (attachedRequest != null) { @@ -118,16 +100,35 @@ namespace osu.Game.Overlays.Direct private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null)); - private void setAdded(BeatmapSetInfo s, bool existing) => setDownloadStateFromManager(s, DownloadState.LocallyAvailable); + private void itemAdded(TModel s) => setDownloadStateFromManager(s, DownloadState.LocallyAvailable); - private void setRemoved(BeatmapSetInfo s) => setDownloadStateFromManager(s, DownloadState.NotDownloaded); + private void itemRemoved(TModel s) => setDownloadStateFromManager(s, DownloadState.NotDownloaded); - private void setDownloadStateFromManager(BeatmapSetInfo s, DownloadState state) => Schedule(() => + private void setDownloadStateFromManager(TModel s, DownloadState state) => Schedule(() => { - if (s.OnlineBeatmapSetID != BeatmapSet.Value?.OnlineBeatmapSetID) + if (!s.Equals(Model.Value)) return; State.Value = state; }); + + #region Disposal + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (manager != null) + { + manager.DownloadBegan -= attachDownload; + manager.ItemAdded -= itemAdded; + } + + State.UnbindAll(); + + attachDownload(null); + } + + #endregion } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 35684849a3..f7f2e1b451 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -29,6 +29,7 @@ using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; @@ -82,6 +83,7 @@ namespace osu.Game private OsuScreenStack screenStack; private VolumeOverlay volume; private OsuLogo osuLogo; + private BackButton backButton; private MainMenu menuScreen; private Intro introScreen; @@ -399,6 +401,16 @@ namespace osu.Game Children = new Drawable[] { screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }, + backButton = new BackButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Action = () => + { + if ((screenStack.CurrentScreen as IOsuScreen)?.AllowBackButton == true) + screenStack.Exit(); + } + }, logoContainer = new Container { RelativeSizeAxes = Axes.Both }, } }, @@ -795,6 +807,11 @@ namespace osu.Game CloseAllOverlays(); else Toolbar.Show(); + + if (newOsuScreen.AllowBackButton) + backButton.Show(); + else + backButton.Hide(); } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 87ff721bbb..3bdf37d769 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -183,7 +183,7 @@ namespace osu.Game } BeatmapManager.ItemRemoved += i => ScoreManager.Delete(getBeatmapScores(i), true); - BeatmapManager.ItemAdded += (i, existing) => ScoreManager.Undelete(getBeatmapScores(i), true); + BeatmapManager.ItemAdded += i => ScoreManager.Undelete(getBeatmapScores(i), true); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs b/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs new file mode 100644 index 0000000000..896c646552 --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osuTK.Graphics; + +namespace osu.Game.Overlays.BeatmapSet +{ + public class BeatmapAvailability : Container + { + private BeatmapSetInfo beatmapSet; + + private bool downloadDisabled => BeatmapSet?.OnlineInfo.Availability?.DownloadDisabled ?? false; + private bool hasExternalLink => !string.IsNullOrEmpty(BeatmapSet?.OnlineInfo.Availability?.ExternalLink); + + private readonly LinkFlowContainer textContainer; + + public BeatmapAvailability() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Padding = new MarginPadding { Top = 10, Right = 20 }; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.6f), + }, + textContainer = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: 14)) + { + Direction = FillDirection.Full, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), + }, + }; + } + + public BeatmapSetInfo BeatmapSet + { + get => beatmapSet; + + set + { + if (value == beatmapSet) + return; + + beatmapSet = value; + + if (downloadDisabled || hasExternalLink) + { + Show(); + updateText(); + } + else + Hide(); + } + } + + private void updateText() + { + textContainer.Clear(); + textContainer.AddParagraph(downloadDisabled + ? "This beatmap is currently not available for download." + : "Portions of this beatmap have been removed at the request of the creator or a third-party rights holder.", t => t.Colour = Color4.Orange); + + if (hasExternalLink) + { + textContainer.NewParagraph(); + textContainer.NewParagraph(); + textContainer.AddLink("Check here for more information.", BeatmapSet.OnlineInfo.Availability.ExternalLink, creationParameters: t => t.Font = OsuFont.GetFont(size: 10)); + } + } + } +} diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs index 0a159507fe..3e8a5a8324 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Overlays.Direct; using osu.Game.Users; @@ -19,7 +20,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays.BeatmapSet.Buttons { - public class DownloadButton : DownloadTrackingComposite, IHasTooltip + public class DownloadButton : BeatmapDownloadTrackingComposite, IHasTooltip { private readonly bool noVideo; diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index fb6c50d867..1c1167d08e 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -8,11 +9,11 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online; using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Overlays.Direct; using osuTK; @@ -21,7 +22,7 @@ using DownloadButton = osu.Game.Overlays.BeatmapSet.Buttons.DownloadButton; namespace osu.Game.Overlays.BeatmapSet { - public class Header : DownloadTrackingComposite + public class Header : BeatmapDownloadTrackingComposite { private const float transition_duration = 200; private const float tabs_height = 50; @@ -33,13 +34,20 @@ namespace osu.Game.Overlays.BeatmapSet private readonly OsuSpriteText title, artist; private readonly AuthorInfo author; private readonly FillFlowContainer downloadButtonsContainer; + private readonly BeatmapAvailability beatmapAvailability; private readonly BeatmapSetOnlineStatusPill onlineStatusPill; public Details Details; + public bool DownloadButtonsVisible => downloadButtonsContainer.Any(); + public readonly BeatmapPicker Picker; private readonly FavouriteButton favouriteButton; + private readonly FillFlowContainer fadeContent; + + private readonly LoadingAnimation loading; + public Header() { ExternalLinkButton externalLink; @@ -105,63 +113,73 @@ namespace osu.Game.Overlays.BeatmapSet Left = BeatmapSetOverlay.X_PADDING, Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH, }, - Child = new FillFlowContainer + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] + fadeContent = new FillFlowContainer { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = Picker = new BeatmapPicker(), - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + new Container { - title = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 37, weight: FontWeight.Bold, italics: true) - }, - externalLink = new ExternalLinkButton - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Left = 3, Bottom = 4 }, //To better lineup with the font - }, - } - }, - artist = new OsuSpriteText { Font = OsuFont.GetFont(size: 25, weight: FontWeight.SemiBold, italics: true) }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 20 }, - Child = author = new AuthorInfo(), - }, - new Container - { - RelativeSizeAxes = Axes.X, - Height = buttons_height, - Margin = new MarginPadding { Top = 10 }, - Children = new Drawable[] + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = Picker = new BeatmapPicker(), + }, + new FillFlowContainer { - favouriteButton = new FavouriteButton(), - downloadButtonsContainer = new FillFlowContainer + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = buttons_height + buttons_spacing }, - Spacing = new Vector2(buttons_spacing), + title = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 37, weight: FontWeight.Bold, italics: true) + }, + externalLink = new ExternalLinkButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Left = 3, Bottom = 4 }, //To better lineup with the font + }, + } + }, + artist = new OsuSpriteText { Font = OsuFont.GetFont(size: 25, weight: FontWeight.SemiBold, italics: true) }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 20 }, + Child = author = new AuthorInfo(), + }, + beatmapAvailability = new BeatmapAvailability(), + new Container + { + RelativeSizeAxes = Axes.X, + Height = buttons_height, + Margin = new MarginPadding { Top = 10 }, + Children = new Drawable[] + { + favouriteButton = new FavouriteButton(), + downloadButtonsContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = buttons_height + buttons_spacing }, + Spacing = new Vector2(buttons_spacing), + }, }, }, }, }, - }, + } + }, + loading = new LoadingAnimation + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.5f), }, new FillFlowContainer { @@ -187,8 +205,11 @@ namespace osu.Game.Overlays.BeatmapSet }, }; - Picker.Beatmap.ValueChanged += b => Details.Beatmap = b.NewValue; - Picker.Beatmap.ValueChanged += b => externalLink.Link = $@"https://osu.ppy.sh/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; + Picker.Beatmap.ValueChanged += b => + { + Details.Beatmap = b.NewValue; + externalLink.Link = $@"https://osu.ppy.sh/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; + }; } [BackgroundDependencyLoader] @@ -200,25 +221,36 @@ namespace osu.Game.Overlays.BeatmapSet BeatmapSet.BindValueChanged(setInfo => { - Picker.BeatmapSet = author.BeatmapSet = Details.BeatmapSet = setInfo.NewValue; - - title.Text = setInfo.NewValue?.Metadata.Title ?? string.Empty; - artist.Text = setInfo.NewValue?.Metadata.Artist ?? string.Empty; - onlineStatusPill.Status = setInfo.NewValue?.OnlineInfo.Status ?? BeatmapSetOnlineStatus.None; + Picker.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue; cover.BeatmapSet = setInfo.NewValue; - if (setInfo.NewValue != null) - { - downloadButtonsContainer.FadeIn(transition_duration); - favouriteButton.FadeIn(transition_duration); - } - else + if (setInfo.NewValue == null) { + onlineStatusPill.FadeTo(0.5f, 500, Easing.OutQuint); + fadeContent.Hide(); + + loading.Show(); + downloadButtonsContainer.FadeOut(transition_duration); favouriteButton.FadeOut(transition_duration); } + else + { + fadeContent.FadeIn(500, Easing.OutQuint); - updateDownloadButtons(); + loading.Hide(); + + title.Text = setInfo.NewValue.Metadata.Title ?? string.Empty; + artist.Text = setInfo.NewValue.Metadata.Artist ?? string.Empty; + + onlineStatusPill.FadeIn(500, Easing.OutQuint); + onlineStatusPill.Status = setInfo.NewValue.OnlineInfo.Status; + + downloadButtonsContainer.FadeIn(transition_duration); + favouriteButton.FadeIn(transition_duration); + + updateDownloadButtons(); + } }, true); } @@ -226,11 +258,17 @@ namespace osu.Game.Overlays.BeatmapSet { if (BeatmapSet.Value == null) return; + if (BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) + { + downloadButtonsContainer.Clear(); + return; + } + switch (State.Value) { case DownloadState.LocallyAvailable: // temporary for UX until new design is implemented. - downloadButtonsContainer.Child = new osu.Game.Overlays.Direct.DownloadButton(BeatmapSet.Value) + downloadButtonsContainer.Child = new Direct.DownloadButton(BeatmapSet.Value) { Width = 50, RelativeSizeAxes = Axes.Y diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index 5ee143850f..1d9c4e7fc8 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Size = new Vector2(40), FillMode = FillMode.Fit, }, - avatar = new UpdateableAvatar + avatar = new UpdateableAvatar(hideImmediately: true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -90,7 +90,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold) }, - flag = new UpdateableFlag + flag = new UpdateableFlag(hideImmediately: true) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 205909ce7d..19f6a3f692 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -27,12 +27,10 @@ namespace osu.Game.Overlays public const float TOP_PADDING = 25; public const float RIGHT_WIDTH = 275; - private readonly Header header; + protected readonly Header Header; private RulesetStore rulesets; - private readonly OsuScrollContainer scroll; - private readonly Bindable beatmapSet = new Bindable(); // receive input outside our bounds so we can trigger a close event on ourselves. @@ -40,6 +38,7 @@ namespace osu.Game.Overlays public BeatmapSetOverlay() { + OsuScrollContainer scroll; Info info; ScoresContainer scores; @@ -61,7 +60,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - header = new Header(), + Header = new Header(), info = new Info(), scores = new ScoresContainer(), }, @@ -69,13 +68,15 @@ namespace osu.Game.Overlays }, }; - header.BeatmapSet.BindTo(beatmapSet); + Header.BeatmapSet.BindTo(beatmapSet); info.BeatmapSet.BindTo(beatmapSet); - header.Picker.Beatmap.ValueChanged += b => + Header.Picker.Beatmap.ValueChanged += b => { info.Beatmap = b.NewValue; scores.Beatmap = b.NewValue; + + scroll.ScrollToStart(); }; } @@ -104,7 +105,7 @@ namespace osu.Game.Overlays req.Success += res => { beatmapSet.Value = res.ToBeatmapSet(rulesets); - header.Picker.Beatmap.Value = header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineBeatmapID == beatmapId); + Header.Picker.Beatmap.Value = Header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineBeatmapID == beatmapId); }; API.Queue(req); Show(); @@ -119,11 +120,14 @@ namespace osu.Game.Overlays Show(); } + /// + /// Show an already fully-populated beatmap set. + /// + /// The set to show. public void ShowBeatmapSet(BeatmapSetInfo set) { beatmapSet.Value = set; Show(); - scroll.ScrollTo(0); } } } diff --git a/osu.Game/Overlays/Direct/BeatmapDownloadTrackingComposite.cs b/osu.Game/Overlays/Direct/BeatmapDownloadTrackingComposite.cs new file mode 100644 index 0000000000..fd04a1541e --- /dev/null +++ b/osu.Game/Overlays/Direct/BeatmapDownloadTrackingComposite.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Online; + +namespace osu.Game.Overlays.Direct +{ + public abstract class BeatmapDownloadTrackingComposite : DownloadTrackingComposite + { + public Bindable BeatmapSet => Model; + + protected BeatmapDownloadTrackingComposite(BeatmapSetInfo set = null) + : base(set) + { + } + } +} diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/Direct/DirectListPanel.cs index b731e95d54..6f3b5bc5f1 100644 --- a/osu.Game/Overlays/Direct/DirectListPanel.cs +++ b/osu.Game/Overlays/Direct/DirectListPanel.cs @@ -27,6 +27,7 @@ namespace osu.Game.Overlays.Direct private const float height = 70; private FillFlowContainer statusContainer; + protected DownloadButton DownloadButton; private PlayButton playButton; private Box progressBar; @@ -149,7 +150,7 @@ namespace osu.Game.Overlays.Direct Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, - Child = new DownloadButton(SetInfo) + Child = DownloadButton = new DownloadButton(SetInfo) { Size = new Vector2(height - vertical_padding * 3), Margin = new MarginPadding { Left = vertical_padding * 2, Right = vertical_padding }, diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index f413dc3771..8199d80528 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -34,6 +35,7 @@ namespace osu.Game.Overlays.Direct public PreviewTrack Preview => PlayButton.Preview; public Bindable PreviewPlaying => PlayButton.Playing; + protected abstract PlayButton PlayButton { get; } protected abstract Box PreviewBar { get; } @@ -43,6 +45,8 @@ namespace osu.Game.Overlays.Direct protected DirectPanel(BeatmapSetInfo setInfo) { + Debug.Assert(setInfo.OnlineBeatmapSetID != null); + SetInfo = setInfo; } @@ -117,12 +121,11 @@ namespace osu.Game.Overlays.Direct protected override bool OnClick(ClickEvent e) { - ShowInformation(); + Debug.Assert(SetInfo.OnlineBeatmapSetID != null); + beatmapSetOverlay?.FetchAndShowBeatmapSet(SetInfo.OnlineBeatmapSetID.Value); return true; } - protected void ShowInformation() => beatmapSetOverlay?.ShowBeatmapSet(SetInfo); - protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Overlays/Direct/DownloadButton.cs b/osu.Game/Overlays/Direct/DownloadButton.cs index 3f44d854e5..81709187e7 100644 --- a/osu.Game/Overlays/Direct/DownloadButton.cs +++ b/osu.Game/Overlays/Direct/DownloadButton.cs @@ -9,21 +9,22 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Online; using osuTK; namespace osu.Game.Overlays.Direct { - public class DownloadButton : DownloadTrackingComposite + public class DownloadButton : BeatmapDownloadTrackingComposite { + protected bool DownloadEnabled => button.Enabled.Value; + private readonly bool noVideo; private readonly SpriteIcon icon; private readonly SpriteIcon checkmark; private readonly Box background; private OsuColour colours; - private readonly ShakeContainer shakeContainer; - private readonly OsuAnimatedButton button; public DownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false) @@ -72,11 +73,18 @@ namespace osu.Game.Overlays.Direct FinishTransforms(true); } - [BackgroundDependencyLoader(permitNulls: true)] + [BackgroundDependencyLoader(true)] private void load(OsuColour colours, OsuGame game, BeatmapManager beatmaps) { this.colours = colours; + if (BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) + { + button.Enabled.Value = false; + button.TooltipText = "This beatmap is currently not available for download."; + return; + } + button.Action = () => { switch (State.Value) diff --git a/osu.Game/Overlays/Direct/DownloadProgressBar.cs b/osu.Game/Overlays/Direct/DownloadProgressBar.cs index 57500b3531..a6cefaae84 100644 --- a/osu.Game/Overlays/Direct/DownloadProgressBar.cs +++ b/osu.Game/Overlays/Direct/DownloadProgressBar.cs @@ -7,11 +7,12 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osu.Game.Online; using osuTK.Graphics; namespace osu.Game.Overlays.Direct { - public class DownloadProgressBar : DownloadTrackingComposite + public class DownloadProgressBar : BeatmapDownloadTrackingComposite { private readonly ProgressBar progressBar; diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index 89d166b788..07040f166d 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Music [BackgroundDependencyLoader] private void load(BeatmapManager beatmaps, IBindable beatmap) { - beatmaps.GetAllUsableBeatmapSets().ForEach(b => addBeatmapSet(b, false)); + beatmaps.GetAllUsableBeatmapSets().ForEach(addBeatmapSet); beatmaps.ItemAdded += addBeatmapSet; beatmaps.ItemRemoved += removeBeatmapSet; @@ -83,11 +83,8 @@ namespace osu.Game.Overlays.Music beatmapBacking.ValueChanged += _ => updateSelectedSet(); } - private void addBeatmapSet(BeatmapSetInfo obj, bool existing) => Schedule(() => + private void addBeatmapSet(BeatmapSetInfo obj) => Schedule(() => { - if (existing) - return; - var newItem = new PlaylistItem(obj) { OnSelect = set => Selected?.Invoke(set) }; items.Add(newItem); diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 36937def2b..29ae5983be 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -218,15 +218,9 @@ namespace osu.Game.Overlays beatmapSets.Insert(index, beatmapSetInfo); } - private void handleBeatmapAdded(BeatmapSetInfo obj, bool existing) - { - if (existing) - return; + private void handleBeatmapAdded(BeatmapSetInfo set) => Schedule(() => beatmapSets.Add(set)); - Schedule(() => beatmapSets.Add(obj)); - } - - private void handleBeatmapRemoved(BeatmapSetInfo obj) => Schedule(() => beatmapSets.RemoveAll(s => s.ID == obj.ID)); + private void handleBeatmapRemoved(BeatmapSetInfo set) => Schedule(() => beatmapSets.RemoveAll(s => s.ID == set.ID)); protected override void LoadComplete() { @@ -259,6 +253,12 @@ namespace osu.Game.Overlays { base.Update(); + if (pendingBeatmapSwitch != null) + { + pendingBeatmapSwitch(); + pendingBeatmapSwitch = null; + } + var track = current?.TrackLoaded ?? false ? current.Track : null; if (track?.IsDummyDevice == false) @@ -368,15 +368,12 @@ namespace osu.Game.Overlays mod.ApplyToClock(track); } - private ScheduledDelegate pendingBeatmapSwitch; + private Action pendingBeatmapSwitch; private void updateDisplay(WorkingBeatmap beatmap, TransformDirection direction) { - //we might be off-screen when this update comes in. - //rather than Scheduling, manually handle this to avoid possible memory contention. - pendingBeatmapSwitch?.Cancel(); - - pendingBeatmapSwitch = Schedule(delegate + // avoid using scheduler as our scheduler may not be run for a long time, holding references to beatmaps. + pendingBeatmapSwitch = delegate { // todo: this can likely be replaced with WorkingBeatmap.GetBeatmapAsync() Task.Run(() => @@ -416,7 +413,7 @@ namespace osu.Game.Overlays playerContainer.Add(newBackground); }); - }); + }; } protected override void PopIn() diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetSelector.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetSelector.cs new file mode 100644 index 0000000000..b6112a6501 --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetSelector.cs @@ -0,0 +1,66 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Rulesets; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public class ProfileRulesetSelector : RulesetSelector + { + private Color4 accentColour = Color4.White; + + public ProfileRulesetSelector() + { + TabContainer.Masking = false; + TabContainer.Spacing = new Vector2(10, 0); + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + accentColour = colours.Seafoam; + + foreach (TabItem tabItem in TabContainer) + ((ProfileRulesetTabItem)tabItem).AccentColour = accentColour; + } + + public void SetDefaultRuleset(RulesetInfo ruleset) + { + // Todo: This method shouldn't exist, but bindables don't provide the concept of observing a change to the default value + foreach (TabItem tabItem in TabContainer) + ((ProfileRulesetTabItem)tabItem).IsDefault = ((ProfileRulesetTabItem)tabItem).Value.ID == ruleset.ID; + } + + public void SelectDefaultRuleset() + { + // Todo: This method shouldn't exist, but bindables don't provide the concept of observing a change to the default value + foreach (TabItem tabItem in TabContainer) + { + if (((ProfileRulesetTabItem)tabItem).IsDefault) + { + Current.Value = ((ProfileRulesetTabItem)tabItem).Value; + return; + } + } + } + + protected override TabItem CreateTabItem(RulesetInfo value) => new ProfileRulesetTabItem(value) + { + AccentColour = accentColour + }; + + protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + }; + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetTabItem.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetTabItem.cs new file mode 100644 index 0000000000..b5170ea3a2 --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetTabItem.cs @@ -0,0 +1,125 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public class ProfileRulesetTabItem : TabItem, IHasAccentColour + { + private readonly OsuSpriteText text; + private readonly SpriteIcon icon; + + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + if (accentColour == value) + return; + + accentColour = value; + + updateState(); + } + } + + private bool isDefault; + + public bool IsDefault + { + get => isDefault; + set + { + if (isDefault == value) + return; + + isDefault = value; + + icon.FadeTo(isDefault ? 1 : 0, 200, Easing.OutQuint); + } + } + + public ProfileRulesetTabItem(RulesetInfo value) + : base(value) + { + AutoSizeAxes = Axes.Both; + + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(3, 0), + Children = new Drawable[] + { + text = new OsuSpriteText + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Text = value.Name, + }, + icon = new SpriteIcon + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Alpha = 0, + AlwaysPresent = true, + Icon = FontAwesome.Solid.Star, + Size = new Vector2(12), + }, + } + }, + new HoverClickSounds() + }; + } + + protected override bool OnHover(HoverEvent e) + { + base.OnHover(e); + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + protected override void OnActivated() => updateState(); + + protected override void OnDeactivated() => updateState(); + + private void updateState() + { + text.Font = text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Medium); + + if (IsHovered || Active.Value) + { + text.FadeColour(Color4.White, 120, Easing.InQuad); + icon.FadeColour(Color4.White, 120, Easing.InQuad); + } + else + { + text.FadeColour(AccentColour, 120, Easing.InQuad); + icon.FadeColour(AccentColour, 120, Easing.InQuad); + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index 7f7545eee3..2c25808170 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -73,7 +73,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio private class AudioDeviceSettingsDropdown : SettingsDropdown { - protected override OsuDropdown CreateDropdown() => new AudioDeviceDropdownControl { Items = Items }; + protected override OsuDropdown CreateDropdown() => new AudioDeviceDropdownControl(); private class AudioDeviceDropdownControl : DropdownControl { diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 36c4fb5252..f4de4c0c41 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -237,7 +237,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private class ResolutionSettingsDropdown : SettingsDropdown { - protected override OsuDropdown CreateDropdown() => new ResolutionDropdownControl { Items = Items }; + protected override OsuDropdown CreateDropdown() => new ResolutionDropdownControl(); private class ResolutionDropdownControl : DropdownControl { diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 100022bd13..5d7542ca2b 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -82,13 +82,7 @@ namespace osu.Game.Overlays.Settings.Sections private void itemRemoved(SkinInfo s) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != s.ID).ToArray()); - private void itemAdded(SkinInfo s, bool existing) - { - if (existing) - return; - - Schedule(() => skinDropdown.Items = skinDropdown.Items.Append(s).ToArray()); - } + private void itemAdded(SkinInfo s) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Append(s).ToArray()); protected override void Dispose(bool isDisposing) { @@ -108,7 +102,7 @@ namespace osu.Game.Overlays.Settings.Sections private class SkinSettingsDropdown : SettingsDropdown { - protected override OsuDropdown CreateDropdown() => new SkinDropdownControl { Items = Items }; + protected override OsuDropdown CreateDropdown() => new SkinDropdownControl(); private class SkinDropdownControl : DropdownControl { diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs index de3f741cd7..167061f485 100644 --- a/osu.Game/Overlays/Settings/SettingsDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs @@ -13,39 +13,23 @@ namespace osu.Game.Overlays.Settings { protected new OsuDropdown Control => (OsuDropdown)base.Control; - private IEnumerable items = Enumerable.Empty(); - public IEnumerable Items { - get => items; - set - { - items = value; - - if (Control != null) - Control.Items = value; - } + get => Control.Items; + set => Control.Items = value; } - private IBindableList itemSource; - public IBindableList ItemSource { - get => itemSource; - set - { - itemSource = value; - - if (Control != null) - Control.ItemSource = value; - } + get => Control.ItemSource; + set => Control.ItemSource = value; } public override IEnumerable FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString())); protected sealed override Drawable CreateControl() => CreateDropdown(); - protected virtual OsuDropdown CreateDropdown() => new DropdownControl { Items = Items, ItemSource = ItemSource }; + protected virtual OsuDropdown CreateDropdown() => new DropdownControl(); protected class DropdownControl : OsuDropdown { diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs index 576be71ee6..7f794e2927 100644 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -34,6 +34,8 @@ namespace osu.Game.Overlays }); } + protected override bool DimMainContent => false; // dimming is handled by main overlay + private class BackButton : OsuClickableContainer, IKeyBindingHandler { private AspectContainer aspect; diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 982fb26b6b..19038c3981 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Input.Events; +using osu.Game.Rulesets; namespace osu.Game.Overlays.Toolbar { @@ -23,6 +24,7 @@ namespace osu.Game.Overlays.Toolbar public Action OnHome; private ToolbarUserButton userButton; + private ToolbarRulesetSelector rulesetSelector; protected override bool BlockPositionalInput => false; @@ -40,7 +42,7 @@ namespace osu.Game.Overlays.Toolbar } [BackgroundDependencyLoader(true)] - private void load(OsuGame osuGame) + private void load(OsuGame osuGame, Bindable parentRuleset) { Children = new Drawable[] { @@ -57,7 +59,7 @@ namespace osu.Game.Overlays.Toolbar { Action = () => OnHome?.Invoke() }, - new ToolbarRulesetSelector() + rulesetSelector = new ToolbarRulesetSelector() } }, new FillFlowContainer @@ -84,6 +86,9 @@ namespace osu.Game.Overlays.Toolbar } }; + // Bound after the selector is added to the hierarchy to give it a chance to load the available rulesets + rulesetSelector.Current.BindTo(parentRuleset); + State.ValueChanged += visibility => { if (overlayActivationMode.Value == OverlayActivation.Disabled) diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetButton.cs deleted file mode 100644 index 87b18ba9f4..0000000000 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetButton.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Effects; -using osu.Game.Rulesets; -using osuTK.Graphics; - -namespace osu.Game.Overlays.Toolbar -{ - public class ToolbarRulesetButton : ToolbarButton - { - private RulesetInfo ruleset; - - public RulesetInfo Ruleset - { - get => ruleset; - set - { - ruleset = value; - - var rInstance = ruleset.CreateInstance(); - - TooltipMain = rInstance.Description; - TooltipSub = $"Play some {rInstance.Description}"; - SetIcon(rInstance.CreateIcon()); - } - } - - public bool Active - { - set - { - if (value) - { - IconContainer.Colour = Color4.White; - IconContainer.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = new Color4(255, 194, 224, 100), - Radius = 15, - Roundness = 15, - }; - } - else - { - IconContainer.Colour = new Color4(255, 194, 224, 255); - IconContainer.EdgeEffect = new EdgeEffectParameters(); - } - } - } - - protected override void LoadComplete() - { - base.LoadComplete(); - IconContainer.Scale *= 1.4f; - } - } -} diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 90412ec1d1..f4272ab15c 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -1,51 +1,43 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osuTK; -using osuTK.Input; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; using osu.Game.Rulesets; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osuTK.Input; +using System.Linq; +using osu.Framework.Allocation; namespace osu.Game.Overlays.Toolbar { - public class ToolbarRulesetSelector : Container + public class ToolbarRulesetSelector : RulesetSelector { private const float padding = 10; - private readonly FillFlowContainer modeButtons; - private readonly Drawable modeButtonLine; - private ToolbarRulesetButton activeButton; - - private RulesetStore rulesets; - private readonly Bindable ruleset = new Bindable(); + protected Drawable ModeButtonLine { get; private set; } public ToolbarRulesetSelector() { RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; + } - Children = new[] + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new[] { - new OpaqueBackground(), - modeButtons = new FillFlowContainer + new OpaqueBackground { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Direction = FillDirection.Horizontal, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Padding = new MarginPadding { Left = padding, Right = padding }, + Depth = 1, }, - modeButtonLine = new Container + ModeButtonLine = new Container { Size = new Vector2(padding * 2 + ToolbarButton.WIDTH, 3), Anchor = Anchor.BottomLeft, @@ -58,35 +50,54 @@ namespace osu.Game.Overlays.Toolbar Radius = 15, Roundness = 15, }, - Children = new[] + Child = new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - } + RelativeSizeAxes = Axes.Both, } } - }; + }); } - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets, Bindable parentRuleset) + protected override void LoadComplete() { - this.rulesets = rulesets; + base.LoadComplete(); - foreach (var r in rulesets.AvailableRulesets) + Current.BindDisabledChanged(disabled => this.FadeColour(disabled ? Color4.Gray : Color4.White, 300), true); + Current.BindValueChanged(_ => moveLineToCurrent(), true); + } + + private bool hasInitialPosition; + + // Scheduled to allow the flow layout to be computed before the line position is updated + private void moveLineToCurrent() => ScheduleAfterChildren(() => + { + foreach (var tabItem in TabContainer) { - modeButtons.Add(new ToolbarRulesetButton + if (tabItem.Value == Current.Value) { - Ruleset = r, - Action = delegate { ruleset.Value = r; } - }); + ModeButtonLine.MoveToX(tabItem.DrawPosition.X, !hasInitialPosition ? 0 : 200, Easing.OutQuint); + break; + } } - ruleset.ValueChanged += rulesetChanged; - ruleset.DisabledChanged += disabledChanged; - ruleset.BindTo(parentRuleset); - } + hasInitialPosition = true; + }); + + public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput; + + public override bool HandlePositionalInput => !Current.Disabled && base.HandlePositionalInput; + + public override bool PropagatePositionalInputSubTree => !Current.Disabled && base.PropagatePositionalInputSubTree; + + protected override TabItem CreateTabItem(RulesetInfo value) => new ToolbarRulesetTabButton(value); + + protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Left = padding, Right = padding }, + }; protected override bool OnKeyDown(KeyDownEvent e) { @@ -96,46 +107,13 @@ namespace osu.Game.Overlays.Toolbar { int requested = e.Key - Key.Number1; - RulesetInfo found = rulesets.AvailableRulesets.Skip(requested).FirstOrDefault(); + RulesetInfo found = Rulesets.AvailableRulesets.Skip(requested).FirstOrDefault(); if (found != null) - ruleset.Value = found; + Current.Value = found; return true; } return false; } - - public override bool HandleNonPositionalInput => !ruleset.Disabled && base.HandleNonPositionalInput; - public override bool HandlePositionalInput => !ruleset.Disabled && base.HandlePositionalInput; - - public override bool PropagatePositionalInputSubTree => !ruleset.Disabled && base.PropagatePositionalInputSubTree; - - private void disabledChanged(bool isDisabled) => this.FadeColour(isDisabled ? Color4.Gray : Color4.White, 300); - - private void rulesetChanged(ValueChangedEvent e) - { - foreach (ToolbarRulesetButton m in modeButtons.Children.Cast()) - { - bool isActive = m.Ruleset.ID == e.NewValue.ID; - m.Active = isActive; - if (isActive) - activeButton = m; - } - - activeMode.Invalidate(); - } - - private Cached activeMode = new Cached(); - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - if (!activeMode.IsValid) - { - modeButtonLine.MoveToX(activeButton.DrawPosition.X, 200, Easing.OutQuint); - activeMode.Validate(); - } - } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs new file mode 100644 index 0000000000..a5194ea752 --- /dev/null +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -0,0 +1,76 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Rulesets; +using osuTK.Graphics; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; + +namespace osu.Game.Overlays.Toolbar +{ + public class ToolbarRulesetTabButton : TabItem + { + private readonly RulesetButton ruleset; + + public ToolbarRulesetTabButton(RulesetInfo value) + : base(value) + { + AutoSizeAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + Child = ruleset = new RulesetButton + { + Active = false, + }; + + var rInstance = value.CreateInstance(); + + ruleset.TooltipMain = rInstance.Description; + ruleset.TooltipSub = $"Play some {rInstance.Description}"; + ruleset.SetIcon(rInstance.CreateIcon()); + } + + protected override void OnActivated() => ruleset.Active = true; + + protected override void OnDeactivated() => ruleset.Active = false; + + private class RulesetButton : ToolbarButton + { + public bool Active + { + set + { + if (value) + { + IconContainer.Colour = Color4.White; + IconContainer.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = new Color4(255, 194, 224, 100), + Radius = 15, + Roundness = 15, + }; + } + else + { + IconContainer.Colour = new Color4(255, 194, 224, 255); + IconContainer.EdgeEffect = new EdgeEffectParameters(); + } + } + } + + protected override bool OnClick(ClickEvent e) + { + Parent.Click(); + return base.OnClick(e); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + IconContainer.Scale *= 1.4f; + } + } + } +} diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 058d84d32f..b924b3302f 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -32,7 +32,8 @@ namespace osu.Game.Overlays public void ShowUser(User user, bool fetchOnline = true) { - if (user == User.SYSTEM_USER) return; + if (user == User.SYSTEM_USER) + return; Show(); @@ -76,7 +77,7 @@ namespace osu.Game.Overlays { Colour = OsuColour.Gray(34), RelativeSizeAxes = Axes.Both - } + }, }); sectionsContainer.SelectedSection.ValueChanged += section => { diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 1cce5598e1..35c3a0e5e4 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -77,10 +77,6 @@ namespace osu.Game.Rulesets.Objects.Drawables private bool judgementOccurred; - public bool Interactive = true; - public override bool HandleNonPositionalInput => Interactive; - public override bool HandlePositionalInput => Interactive; - public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; protected override bool RequiresChildrenUpdate => true; diff --git a/osu.Game/Rulesets/RulesetSelector.cs b/osu.Game/Rulesets/RulesetSelector.cs new file mode 100644 index 0000000000..8e6ec556d2 --- /dev/null +++ b/osu.Game/Rulesets/RulesetSelector.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Allocation; + +namespace osu.Game.Rulesets +{ + public abstract class RulesetSelector : TabControl + { + [Resolved] + protected RulesetStore Rulesets { get; private set; } + + protected override Dropdown CreateDropdown() => null; + + [BackgroundDependencyLoader] + private void load() + { + foreach (var r in Rulesets.AvailableRulesets) + AddItem(r); + } + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index de0f3870c6..660c1235d1 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -32,6 +32,8 @@ namespace osu.Game.Screens.Edit { protected override BackgroundScreen CreateBackground() => new BackgroundScreenCustom(@"Backgrounds/bg4"); + public override bool AllowBackButton => false; + public override bool HideOverlaysOnEnter => true; public override bool DisallowExternalBeatmapRulesetChanges => true; diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index b25bcdeab1..9fc907c2a4 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -17,6 +17,11 @@ namespace osu.Game.Screens /// bool DisallowExternalBeatmapRulesetChanges { get; } + /// + /// Whether the user can exit this this by pressing the back button. + /// + bool AllowBackButton { get; } + /// /// Whether a top-level component should be allowed to exit the current screen to, for example, /// complete an import. Note that this can be overridden by a user if they specifically request. diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index fbe4b6311e..8add730c4e 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -9,22 +9,13 @@ using osu.Framework.Graphics.Shaders; using osu.Game.Screens.Menu; using osuTK; using osu.Framework.Screens; -using osu.Game.Overlays; namespace osu.Game.Screens { - public class Loader : OsuScreen + public class Loader : StartupScreen { private bool showDisclaimer; - public override bool HideOverlaysOnEnter => true; - - public override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; - - public override bool CursorVisible => false; - - protected override bool AllowBackButton => false; - public Loader() { ValidForResume = false; diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 0130a5143b..97231a1331 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -15,12 +15,11 @@ using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osuTK; using osuTK.Graphics; -using osu.Game.Overlays; using osu.Game.Users; namespace osu.Game.Screens.Menu { - public class Disclaimer : OsuScreen + public class Disclaimer : StartupScreen { private Intro intro; private SpriteIcon icon; @@ -28,11 +27,6 @@ namespace osu.Game.Screens.Menu private LinkFlowContainer textFlow; private LinkFlowContainer supportFlow; - public override bool HideOverlaysOnEnter => true; - public override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; - - public override bool CursorVisible => false; - private Drawable heart; private const float icon_y = -85; diff --git a/osu.Game/Screens/Menu/Intro.cs b/osu.Game/Screens/Menu/Intro.cs index 88537322ad..c52e8541c5 100644 --- a/osu.Game/Screens/Menu/Intro.cs +++ b/osu.Game/Screens/Menu/Intro.cs @@ -15,11 +15,10 @@ using osu.Game.IO.Archives; using osu.Game.Screens.Backgrounds; using osuTK; using osuTK.Graphics; -using osu.Game.Overlays; namespace osu.Game.Screens.Menu { - public class Intro : OsuScreen + public class Intro : StartupScreen { private const string menu_music_beatmap_hash = "3c8b1fcc9434dbb29e2fb613d3b9eada9d7bb6c125ceb32396c3b53437280c83"; @@ -32,11 +31,6 @@ namespace osu.Game.Screens.Menu private SampleChannel welcome; private SampleChannel seeya; - public override bool HideOverlaysOnEnter => true; - public override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; - - public override bool CursorVisible => false; - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBlack(); private Bindable menuVoice; diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 65db2973de..7e6de54d1b 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Menu public override bool HideOverlaysOnEnter => buttons == null || buttons.State == ButtonSystemState.Initial; - protected override bool AllowBackButton => buttons.State != ButtonSystemState.Initial && host.CanExit; + public override bool AllowBackButton => false; public override bool AllowExternalScreenChange => true; diff --git a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs index 86b4cfbfa7..8ab0b8f61f 100644 --- a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs @@ -54,11 +54,8 @@ namespace osu.Game.Screens.Multi.Match.Components hasBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID) != null; } - private void beatmapAdded(BeatmapSetInfo model, bool existing) + private void beatmapAdded(BeatmapSetInfo model) { - if (Beatmap.Value == null) - return; - if (model.Beatmaps.Any(b => b.OnlineBeatmapID == Beatmap.Value.OnlineBeatmapID)) Schedule(() => hasBeatmap = true); } diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index a80056d164..5469d4c8c8 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -195,6 +195,7 @@ namespace osu.Game.Screens.Multi.Match Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); Mods.Value = e.NewValue?.RequiredMods?.ToArray() ?? Array.Empty(); + if (e.NewValue?.Ruleset != null) Ruleset.Value = e.NewValue.Ruleset; } @@ -202,7 +203,7 @@ namespace osu.Game.Screens.Multi.Match /// /// Handle the case where a beatmap is imported (and can be used by this match). /// - private void beatmapAdded(BeatmapSetInfo model, bool existing) => Schedule(() => + private void beatmapAdded(BeatmapSetInfo model) => Schedule(() => { if (Beatmap.Value != beatmapManager.DefaultBeatmap) return; diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index a56d153646..9ffd620e55 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -212,6 +212,12 @@ namespace osu.Game.Screens.Multi public override bool OnExiting(IScreen next) { + if (!(screenStack.CurrentScreen is LoungeSubScreen)) + { + screenStack.Exit(); + return true; + } + waves.Hide(); this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index e2aeb41de1..328631ff9c 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -8,10 +8,8 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Input.Bindings; using osu.Framework.Screens; using osu.Game.Beatmaps; -using osu.Game.Input.Bindings; using osu.Game.Rulesets; using osu.Game.Screens.Menu; using osu.Game.Overlays; @@ -21,7 +19,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Screens { - public abstract class OsuScreen : Screen, IOsuScreen, IKeyBindingHandler, IHasDescription + public abstract class OsuScreen : Screen, IOsuScreen, IHasDescription { /// /// The amount of negative padding that should be applied to game background content which touches both the left and right sides of the screen. @@ -36,7 +34,7 @@ namespace osu.Game.Screens public string Description => Title; - protected virtual bool AllowBackButton => true; + public virtual bool AllowBackButton => true; public virtual bool AllowExternalScreenChange => false; @@ -131,21 +129,6 @@ namespace osu.Game.Screens sampleExit = audio.Samples.Get(@"UI/screen-back"); } - public virtual bool OnPressed(GlobalAction action) - { - if (!this.IsCurrentScreen()) return false; - - if (action == GlobalAction.Back && AllowBackButton) - { - this.Exit(); - return true; - } - - return false; - } - - public bool OnReleased(GlobalAction action) => action == GlobalAction.Back && AllowBackButton; - public override void OnResuming(IScreen last) { if (PlayResumeSound) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c3a9ffdaba..957498dc44 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play { public class Player : ScreenWithBeatmapBackground { - protected override bool AllowBackButton => false; // handled by HoldForMenuButton + public override bool AllowBackButton => false; // handled by HoldForMenuButton protected override UserActivity InitialActivity => new UserActivity.SoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; fadeOut(true); - performUserRequestedExit(); + performImmediateExit(); }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, } @@ -251,15 +251,20 @@ namespace osu.Game.Screens.Play return working; } - private void performUserRequestedExit() + private void performImmediateExit() { - if (!this.IsCurrentScreen()) return; - // if a restart has been requested, cancel any pending completion (user has shown intent to restart). onCompletionEvent = null; ValidForResume = false; + performUserRequestedExit(); + } + + private void performUserRequestedExit() + { + if (!this.IsCurrentScreen()) return; + this.Exit(); } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 9de9f5cec8..681ce701d0 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -247,6 +247,7 @@ namespace osu.Game.Screens.Play public override void OnSuspending(IScreen next) { + BackgroundBrightnessReduction = false; base.OnSuspending(next); cancelLoad(); } @@ -258,6 +259,7 @@ namespace osu.Game.Screens.Play cancelLoad(); Background.EnableUserDim.Value = false; + BackgroundBrightnessReduction = false; return base.OnExiting(next); } @@ -273,6 +275,22 @@ namespace osu.Game.Screens.Play } } + private bool backgroundBrightnessReduction; + + protected bool BackgroundBrightnessReduction + { + get => backgroundBrightnessReduction; + set + { + if (value == backgroundBrightnessReduction) + return; + + backgroundBrightnessReduction = value; + + Background.FadeColour(OsuColour.Gray(backgroundBrightnessReduction ? 0.8f : 1), 200); + } + } + protected override void Update() { base.Update(); @@ -287,12 +305,16 @@ namespace osu.Game.Screens.Play // Preview user-defined background dim and blur when hovered on the visual settings panel. Background.EnableUserDim.Value = true; Background.BlurAmount.Value = 0; + + BackgroundBrightnessReduction = false; } else { // Returns background dim and blur to the values specified by PlayerLoader. Background.EnableUserDim.Value = false; Background.BlurAmount.Value = BACKGROUND_BLUR; + + BackgroundBrightnessReduction = true; } } diff --git a/osu.Game/Screens/Ranking/Results.cs b/osu.Game/Screens/Ranking/Results.cs index 370c856d1d..f53fb75e1a 100644 --- a/osu.Game/Screens/Ranking/Results.cs +++ b/osu.Game/Screens/Ranking/Results.cs @@ -16,7 +16,6 @@ using osu.Game.Screens.Backgrounds; using osuTK; using osuTK.Graphics; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Sprites; using osu.Game.Scoring; @@ -254,13 +253,7 @@ namespace osu.Game.Screens.Ranking } } } - }, - new BackButton - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Action = this.Exit - }, + } }; foreach (var t in CreateResultPages()) diff --git a/osu.Game/Screens/ScreenWhiteBox.cs b/osu.Game/Screens/ScreenWhiteBox.cs index d6766c2b49..5648dd997b 100644 --- a/osu.Game/Screens/ScreenWhiteBox.cs +++ b/osu.Game/Screens/ScreenWhiteBox.cs @@ -20,8 +20,6 @@ namespace osu.Game.Screens { public class ScreenWhiteBox : OsuScreen { - private readonly BackButton popButton; - private const double transition_time = 1000; protected virtual IEnumerable PossibleChildren => null; @@ -35,10 +33,6 @@ namespace osu.Game.Screens { base.OnEntering(last); - //only show the pop button if we are entered form another screen. - if (last != null) - popButton.Alpha = 1; - Alpha = 0; textContainer.Position = new Vector2(DrawSize.X / 16, 0); @@ -144,13 +138,6 @@ namespace osu.Game.Screens }, } }, - popButton = new BackButton - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Alpha = 0, - Action = this.Exit - }, childModeButtons = new FillFlowContainer { Direction = FillDirection.Vertical, diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index cf21c78c7f..a6fbb1ff94 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -148,40 +148,37 @@ namespace osu.Game.Screens.Select }); } - public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) + public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { - Schedule(() => + int? previouslySelectedID = null; + CarouselBeatmapSet existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); + + // If the selected beatmap is about to be removed, store its ID so it can be re-selected if required + if (existingSet?.State?.Value == CarouselItemState.Selected) + previouslySelectedID = selectedBeatmap?.Beatmap.ID; + + var newSet = createCarouselSet(beatmapSet); + + if (existingSet != null) + root.RemoveChild(existingSet); + + if (newSet == null) { - int? previouslySelectedID = null; - CarouselBeatmapSet existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); - - // If the selected beatmap is about to be removed, store its ID so it can be re-selected if required - if (existingSet?.State?.Value == CarouselItemState.Selected) - previouslySelectedID = selectedBeatmap?.Beatmap.ID; - - var newSet = createCarouselSet(beatmapSet); - - if (existingSet != null) - root.RemoveChild(existingSet); - - if (newSet == null) - { - itemsCache.Invalidate(); - return; - } - - root.AddChild(newSet); - - applyActiveCriteria(false, false); - - //check if we can/need to maintain our current selection. - if (previouslySelectedID != null) - select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == previouslySelectedID) ?? newSet); - itemsCache.Invalidate(); - Schedule(() => BeatmapSetsChanged?.Invoke()); - }); - } + return; + } + + root.AddChild(newSet); + + applyActiveCriteria(false, false); + + //check if we can/need to maintain our current selection. + if (previouslySelectedID != null) + select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == previouslySelectedID) ?? newSet); + + itemsCache.Invalidate(); + Schedule(() => BeatmapSetsChanged?.Invoke()); + }); /// /// Selects a given beatmap on the carousel. diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs index 6ba29751b0..0680711f1c 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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 osuTK; @@ -25,8 +24,6 @@ namespace osu.Game.Screens.Select private const float padding = 80; - public Action OnBack; - private readonly FillFlowContainer buttons; private readonly List overlays = new List(); @@ -83,12 +80,6 @@ namespace osu.Game.Screens.Select Height = 3, Position = new Vector2(0, -3), }, - new BackButton - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Action = () => OnBack?.Invoke() - }, new FillFlowContainer { Anchor = Anchor.BottomLeft, diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index f9df8c3a39..7c62a49a97 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -34,10 +34,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; namespace osu.Game.Screens.Select { - public abstract class SongSelect : OsuScreen + public abstract class SongSelect : OsuScreen, IKeyBindingHandler { private static readonly Vector2 wedged_container_size = new Vector2(0.5f, 245); @@ -186,31 +187,27 @@ namespace osu.Game.Screens.Select if (ShowFooter) { - AddInternal(FooterPanels = new Container + AddRangeInternal(new[] { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding - { - Bottom = Footer.HEIGHT, - }, - }); - AddInternal(Footer = new Footer - { - OnBack = ExitFromBack, - }); - - FooterPanels.AddRange(new Drawable[] - { - BeatmapOptions = new BeatmapOptionsOverlay(), - ModSelect = new ModSelectOverlay + FooterPanels = new Container { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.X, - Origin = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre, - } + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = Footer.HEIGHT }, + Children = new Drawable[] + { + BeatmapOptions = new BeatmapOptionsOverlay(), + ModSelect = new ModSelectOverlay + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + } + } + }, + Footer = new Footer() }); } @@ -277,17 +274,6 @@ namespace osu.Game.Screens.Select return dependencies; } - protected virtual void ExitFromBack() - { - if (ModSelect.State.Value == Visibility.Visible) - { - ModSelect.Hide(); - return; - } - - this.Exit(); - } - public void Edit(BeatmapInfo beatmap = null) { Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce); @@ -518,6 +504,12 @@ namespace osu.Game.Screens.Select public override bool OnExiting(IScreen next) { + if (ModSelect.State.Value == Visibility.Visible) + { + ModSelect.Hide(); + return true; + } + if (base.OnExiting(next)) return true; @@ -586,7 +578,7 @@ namespace osu.Game.Screens.Select } } - private void onBeatmapSetAdded(BeatmapSetInfo s, bool existing) => Carousel.UpdateBeatmapSet(s); + private void onBeatmapSetAdded(BeatmapSetInfo s) => Carousel.UpdateBeatmapSet(s); private void onBeatmapSetRemoved(BeatmapSetInfo s) => Carousel.RemoveBeatmapSet(s); private void onBeatmapRestored(BeatmapInfo b) => Carousel.UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID)); private void onBeatmapHidden(BeatmapInfo b) => Carousel.UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID)); @@ -649,7 +641,7 @@ namespace osu.Game.Screens.Select Schedule(() => BeatmapDetails.Leaderboard.RefreshScores()))); } - public override bool OnPressed(GlobalAction action) + public virtual bool OnPressed(GlobalAction action) { if (!this.IsCurrentScreen()) return false; @@ -660,9 +652,11 @@ namespace osu.Game.Screens.Select return true; } - return base.OnPressed(action); + return false; } + public bool OnReleased(GlobalAction action) => action == GlobalAction.Select; + protected override bool OnKeyDown(KeyDownEvent e) { if (e.Repeat) return false; diff --git a/osu.Game/Screens/StartupScreen.cs b/osu.Game/Screens/StartupScreen.cs new file mode 100644 index 0000000000..797f185a37 --- /dev/null +++ b/osu.Game/Screens/StartupScreen.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Overlays; + +namespace osu.Game.Screens +{ + /// + /// A screen which is shown once as part of the startup procedure. + /// + public abstract class StartupScreen : OsuScreen + { + public override bool AllowBackButton => false; + + public override bool HideOverlaysOnEnter => true; + + public override bool CursorVisible => false; + + public override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; + } +} diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 795b90ba11..a49f2d079b 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Transforms; namespace osu.Game.Users.Drawables { @@ -37,6 +38,8 @@ namespace osu.Game.Users.Drawables set => base.EdgeEffect = value; } + protected override bool TransformImmediately { get; } + /// /// Whether to show a default guest representation on null user (as opposed to nothing). /// @@ -47,11 +50,14 @@ namespace osu.Game.Users.Drawables /// public readonly BindableBool OpenOnClick = new BindableBool(true); - public UpdateableAvatar(User user = null) + public UpdateableAvatar(User user = null, bool hideImmediately = false) { + TransformImmediately = hideImmediately; User = user; } + protected override TransformSequence ApplyHideTransforms(Drawable drawable) => TransformImmediately ? drawable?.FadeOut() : base.ApplyHideTransforms(drawable); + protected override Drawable CreateDrawable(User user) { if (user == null && !ShowGuestOnNull) diff --git a/osu.Game/Users/Drawables/UpdateableFlag.cs b/osu.Game/Users/Drawables/UpdateableFlag.cs index abc16b2390..78d1a8de20 100644 --- a/osu.Game/Users/Drawables/UpdateableFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableFlag.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Transforms; namespace osu.Game.Users.Drawables { @@ -14,16 +15,21 @@ namespace osu.Game.Users.Drawables set => Model = value; } + protected override bool TransformImmediately { get; } + /// /// Whether to show a place holder on null country. /// public bool ShowPlaceholderOnNull = true; - public UpdateableFlag(Country country = null) + public UpdateableFlag(Country country = null, bool hideImmediately = false) { + TransformImmediately = hideImmediately; Country = country; } + protected override TransformSequence ApplyHideTransforms(Drawable drawable) => TransformImmediately ? drawable?.FadeOut() : base.ApplyHideTransforms(drawable); + protected override Drawable CreateDrawable(Country country) { if (country == null && !ShowPlaceholderOnNull) diff --git a/osu.Game/Utils/ZipUtils.cs b/osu.Game/Utils/ZipUtils.cs index 98e24590a8..cd4d876451 100644 --- a/osu.Game/Utils/ZipUtils.cs +++ b/osu.Game/Utils/ZipUtils.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.IO; using SharpCompress.Archives.Zip; namespace osu.Game.Utils @@ -10,6 +11,9 @@ namespace osu.Game.Utils { public static bool IsZipArchive(string path) { + if (!File.Exists(path)) + return false; + try { using (var arc = ZipArchive.Open(path)) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 563e90cec9..b4af007447 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -15,7 +15,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index a36638cf84..9f21af05a1 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -105,8 +105,8 @@ - - + +