mirror of
https://github.com/ppy/osu.git
synced 2024-11-07 12:27:25 +08:00
Merge pull request #10079 from smoogipoo/collection-database
This commit is contained in:
commit
aae8b29a71
221
osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
Normal file
221
osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Game.Collections;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Collections.IO
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class ImportCollectionsTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public async Task TestImportEmptyDatabase()
|
||||||
|
{
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = loadOsu(host);
|
||||||
|
|
||||||
|
await osu.CollectionManager.Import(new MemoryStream());
|
||||||
|
|
||||||
|
Assert.That(osu.CollectionManager.Collections.Count, Is.Zero);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task TestImportWithNoBeatmaps()
|
||||||
|
{
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = loadOsu(host);
|
||||||
|
|
||||||
|
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
|
||||||
|
|
||||||
|
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
|
||||||
|
|
||||||
|
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
|
||||||
|
Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.Zero);
|
||||||
|
|
||||||
|
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
|
||||||
|
Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.Zero);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task TestImportWithBeatmaps()
|
||||||
|
{
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = loadOsu(host, true);
|
||||||
|
|
||||||
|
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
|
||||||
|
|
||||||
|
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
|
||||||
|
|
||||||
|
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
|
||||||
|
Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(1));
|
||||||
|
|
||||||
|
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
|
||||||
|
Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(12));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task TestImportMalformedDatabase()
|
||||||
|
{
|
||||||
|
bool exceptionThrown = false;
|
||||||
|
UnhandledExceptionEventHandler setException = (_, __) => exceptionThrown = true;
|
||||||
|
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += setException;
|
||||||
|
|
||||||
|
var osu = loadOsu(host, true);
|
||||||
|
|
||||||
|
using (var ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
using (var bw = new BinaryWriter(ms, Encoding.UTF8, true))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 10000; i++)
|
||||||
|
bw.Write((byte)i);
|
||||||
|
}
|
||||||
|
|
||||||
|
ms.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
await osu.CollectionManager.Import(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.That(host.UpdateThread.Running, Is.True);
|
||||||
|
Assert.That(exceptionThrown, Is.False);
|
||||||
|
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
AppDomain.CurrentDomain.UnhandledException -= setException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task TestSaveAndReload()
|
||||||
|
{
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = loadOsu(host, true);
|
||||||
|
|
||||||
|
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
|
||||||
|
|
||||||
|
// Move first beatmap from second collection into the first.
|
||||||
|
osu.CollectionManager.Collections[0].Beatmaps.Add(osu.CollectionManager.Collections[1].Beatmaps[0]);
|
||||||
|
osu.CollectionManager.Collections[1].Beatmaps.RemoveAt(0);
|
||||||
|
|
||||||
|
// Rename the second collecction.
|
||||||
|
osu.CollectionManager.Collections[1].Name.Value = "Another";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (HeadlessGameHost host = new HeadlessGameHost("TestSaveAndReload"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = loadOsu(host, true);
|
||||||
|
|
||||||
|
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
|
||||||
|
|
||||||
|
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
|
||||||
|
Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(2));
|
||||||
|
|
||||||
|
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Another"));
|
||||||
|
Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(11));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestOsuGameBase loadOsu(GameHost host, bool withBeatmap = false)
|
||||||
|
{
|
||||||
|
var osu = new TestOsuGameBase(withBeatmap);
|
||||||
|
|
||||||
|
#pragma warning disable 4014
|
||||||
|
Task.Run(() => host.Run(osu));
|
||||||
|
#pragma warning restore 4014
|
||||||
|
|
||||||
|
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
|
||||||
|
|
||||||
|
return osu;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
|
||||||
|
{
|
||||||
|
Task task = Task.Run(() =>
|
||||||
|
{
|
||||||
|
while (!result()) Thread.Sleep(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.IsTrue(task.Wait(timeout), failureMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestOsuGameBase : OsuGameBase
|
||||||
|
{
|
||||||
|
public CollectionManager CollectionManager { get; private set; }
|
||||||
|
|
||||||
|
private readonly bool withBeatmap;
|
||||||
|
|
||||||
|
public TestOsuGameBase(bool withBeatmap)
|
||||||
|
{
|
||||||
|
this.withBeatmap = withBeatmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
// Beatmap must be imported before the collection manager is loaded.
|
||||||
|
if (withBeatmap)
|
||||||
|
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
|
||||||
|
|
||||||
|
AddInternal(CollectionManager = new CollectionManager(Storage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
osu.Game.Tests/Resources/Collections/collections.db
Normal file
BIN
osu.Game.Tests/Resources/Collections/collections.db
Normal file
Binary file not shown.
@ -0,0 +1,244 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Collections;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Dialog;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Collections
|
||||||
|
{
|
||||||
|
public class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene
|
||||||
|
{
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
|
||||||
|
private readonly Container content;
|
||||||
|
private readonly DialogOverlay dialogOverlay;
|
||||||
|
private readonly CollectionManager manager;
|
||||||
|
|
||||||
|
private RulesetStore rulesets;
|
||||||
|
private BeatmapManager beatmapManager;
|
||||||
|
|
||||||
|
private ManageCollectionsDialog dialog;
|
||||||
|
|
||||||
|
public TestSceneManageCollectionsDialog()
|
||||||
|
{
|
||||||
|
base.Content.AddRange(new Drawable[]
|
||||||
|
{
|
||||||
|
manager = new CollectionManager(LocalStorage),
|
||||||
|
content = new Container { RelativeSizeAxes = Axes.Both },
|
||||||
|
dialogOverlay = new DialogOverlay()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(GameHost host)
|
||||||
|
{
|
||||||
|
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
|
||||||
|
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
|
||||||
|
|
||||||
|
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||||
|
{
|
||||||
|
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||||
|
dependencies.Cache(manager);
|
||||||
|
dependencies.Cache(dialogOverlay);
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp() => Schedule(() =>
|
||||||
|
{
|
||||||
|
manager.Collections.Clear();
|
||||||
|
Child = dialog = new ManageCollectionsDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("show dialog", () => dialog.Show());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHideDialog()
|
||||||
|
{
|
||||||
|
AddWaitStep("wait for animation", 3);
|
||||||
|
AddStep("hide dialog", () => dialog.Hide());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLastItemIsPlaceholder()
|
||||||
|
{
|
||||||
|
AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddCollectionExternal()
|
||||||
|
{
|
||||||
|
AddStep("add collection", () => manager.Collections.Add(new BeatmapCollection { Name = { Value = "First collection" } }));
|
||||||
|
assertCollectionCount(1);
|
||||||
|
assertCollectionName(0, "First collection");
|
||||||
|
|
||||||
|
AddStep("add another collection", () => manager.Collections.Add(new BeatmapCollection { Name = { Value = "Second collection" } }));
|
||||||
|
assertCollectionCount(2);
|
||||||
|
assertCollectionName(1, "Second collection");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestFocusPlaceholderDoesNotCreateCollection()
|
||||||
|
{
|
||||||
|
AddStep("focus placeholder", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem>().Last());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertCollectionCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddCollectionViaPlaceholder()
|
||||||
|
{
|
||||||
|
DrawableCollectionListItem placeholderItem = null;
|
||||||
|
|
||||||
|
AddStep("focus placeholder", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(placeholderItem = dialog.ChildrenOfType<DrawableCollectionListItem>().Last());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Done directly via the collection since InputManager methods cannot add text to textbox...
|
||||||
|
AddStep("change collection name", () => placeholderItem.Model.Name.Value = "a");
|
||||||
|
assertCollectionCount(1);
|
||||||
|
AddAssert("collection now exists", () => manager.Collections.Contains(placeholderItem.Model));
|
||||||
|
|
||||||
|
AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRemoveCollectionExternal()
|
||||||
|
{
|
||||||
|
AddStep("add two collections", () => manager.Collections.AddRange(new[]
|
||||||
|
{
|
||||||
|
new BeatmapCollection { Name = { Value = "1" } },
|
||||||
|
new BeatmapCollection { Name = { Value = "2" } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("remove first collection", () => manager.Collections.RemoveAt(0));
|
||||||
|
assertCollectionCount(1);
|
||||||
|
assertCollectionName(0, "2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRemoveCollectionViaButton()
|
||||||
|
{
|
||||||
|
AddStep("add two collections", () => manager.Collections.AddRange(new[]
|
||||||
|
{
|
||||||
|
new BeatmapCollection { Name = { Value = "1" } },
|
||||||
|
new BeatmapCollection { Name = { Value = "2" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
assertCollectionCount(2);
|
||||||
|
|
||||||
|
AddStep("click first delete button", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("dialog not displayed", () => dialogOverlay.CurrentDialog == null);
|
||||||
|
assertCollectionCount(1);
|
||||||
|
assertCollectionName(0, "2");
|
||||||
|
|
||||||
|
AddStep("click first delete button", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is DeleteCollectionDialog);
|
||||||
|
AddStep("click confirmation", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertCollectionCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCollectionNotRemovedWhenDialogCancelled()
|
||||||
|
{
|
||||||
|
AddStep("add two collections", () => manager.Collections.AddRange(new[]
|
||||||
|
{
|
||||||
|
new BeatmapCollection { Name = { Value = "1" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
assertCollectionCount(1);
|
||||||
|
|
||||||
|
AddStep("click first delete button", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is DeleteCollectionDialog);
|
||||||
|
AddStep("click cancellation", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().Last());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertCollectionCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCollectionRenamedExternal()
|
||||||
|
{
|
||||||
|
AddStep("add two collections", () => manager.Collections.AddRange(new[]
|
||||||
|
{
|
||||||
|
new BeatmapCollection { Name = { Value = "1" } },
|
||||||
|
new BeatmapCollection { Name = { Value = "2" } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("change first collection name", () => manager.Collections[0].Name.Value = "First");
|
||||||
|
|
||||||
|
assertCollectionName(0, "First");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCollectionRenamedOnTextChange()
|
||||||
|
{
|
||||||
|
AddStep("add two collections", () => manager.Collections.AddRange(new[]
|
||||||
|
{
|
||||||
|
new BeatmapCollection { Name = { Value = "1" } },
|
||||||
|
new BeatmapCollection { Name = { Value = "2" } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
assertCollectionCount(2);
|
||||||
|
|
||||||
|
AddStep("change first collection name", () => dialog.ChildrenOfType<TextBox>().First().Text = "First");
|
||||||
|
AddAssert("collection has new name", () => manager.Collections[0].Name.Value == "First");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertCollectionCount(int count)
|
||||||
|
=> AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType<DrawableCollectionListItem>().Count(i => i.IsCreated.Value) == count);
|
||||||
|
|
||||||
|
private void assertCollectionName(int index, string name)
|
||||||
|
=> AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType<DrawableCollectionListItem>().ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);
|
||||||
|
}
|
||||||
|
}
|
237
osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
Normal file
237
osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Collections;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.SongSelect
|
||||||
|
{
|
||||||
|
public class TestSceneFilterControl : OsuManualInputManagerTestScene
|
||||||
|
{
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
private readonly Container content;
|
||||||
|
|
||||||
|
private readonly CollectionManager collectionManager;
|
||||||
|
|
||||||
|
private RulesetStore rulesets;
|
||||||
|
private BeatmapManager beatmapManager;
|
||||||
|
|
||||||
|
private FilterControl control;
|
||||||
|
|
||||||
|
public TestSceneFilterControl()
|
||||||
|
{
|
||||||
|
base.Content.AddRange(new Drawable[]
|
||||||
|
{
|
||||||
|
collectionManager = new CollectionManager(LocalStorage),
|
||||||
|
content = new Container { RelativeSizeAxes = Axes.Both }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(GameHost host)
|
||||||
|
{
|
||||||
|
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
|
||||||
|
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
|
||||||
|
|
||||||
|
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||||
|
{
|
||||||
|
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||||
|
dependencies.Cache(collectionManager);
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp() => Schedule(() =>
|
||||||
|
{
|
||||||
|
collectionManager.Collections.Clear();
|
||||||
|
|
||||||
|
Child = control = new FilterControl
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Height = FilterControl.HEIGHT,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEmptyCollectionFilterContainsAllBeatmaps()
|
||||||
|
{
|
||||||
|
assertCollectionDropdownContains("All beatmaps");
|
||||||
|
assertCollectionHeaderDisplays("All beatmaps");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCollectionAddedToDropdown()
|
||||||
|
{
|
||||||
|
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
||||||
|
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } }));
|
||||||
|
assertCollectionDropdownContains("1");
|
||||||
|
assertCollectionDropdownContains("2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCollectionRemovedFromDropdown()
|
||||||
|
{
|
||||||
|
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
||||||
|
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } }));
|
||||||
|
AddStep("remove collection", () => collectionManager.Collections.RemoveAt(0));
|
||||||
|
|
||||||
|
assertCollectionDropdownContains("1", false);
|
||||||
|
assertCollectionDropdownContains("2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCollectionRenamed()
|
||||||
|
{
|
||||||
|
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
||||||
|
AddStep("select collection", () =>
|
||||||
|
{
|
||||||
|
var dropdown = control.ChildrenOfType<CollectionFilterDropdown>().Single();
|
||||||
|
dropdown.Current.Value = dropdown.ItemSource.ElementAt(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
addExpandHeaderStep();
|
||||||
|
|
||||||
|
AddStep("change name", () => collectionManager.Collections[0].Name.Value = "First");
|
||||||
|
|
||||||
|
assertCollectionDropdownContains("First");
|
||||||
|
assertCollectionHeaderDisplays("First");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAllBeatmapFilterDoesNotHaveAddButton()
|
||||||
|
{
|
||||||
|
addExpandHeaderStep();
|
||||||
|
AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0)));
|
||||||
|
AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCollectionFilterHasAddButton()
|
||||||
|
{
|
||||||
|
addExpandHeaderStep();
|
||||||
|
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
||||||
|
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
|
||||||
|
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestButtonDisabledAndEnabledWithBeatmapChanges()
|
||||||
|
{
|
||||||
|
addExpandHeaderStep();
|
||||||
|
|
||||||
|
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
||||||
|
|
||||||
|
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||||
|
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
|
||||||
|
|
||||||
|
AddStep("set dummy beatmap", () => Beatmap.SetDefault());
|
||||||
|
AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestButtonChangesWhenAddedAndRemovedFromCollection()
|
||||||
|
{
|
||||||
|
addExpandHeaderStep();
|
||||||
|
|
||||||
|
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||||
|
|
||||||
|
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
||||||
|
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
|
||||||
|
|
||||||
|
AddStep("add beatmap to collection", () => collectionManager.Collections[0].Beatmaps.Add(Beatmap.Value.BeatmapInfo));
|
||||||
|
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
|
||||||
|
|
||||||
|
AddStep("remove beatmap from collection", () => collectionManager.Collections[0].Beatmaps.Clear());
|
||||||
|
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestButtonAddsAndRemovesBeatmap()
|
||||||
|
{
|
||||||
|
addExpandHeaderStep();
|
||||||
|
|
||||||
|
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||||
|
|
||||||
|
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
||||||
|
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
|
||||||
|
|
||||||
|
addClickAddOrRemoveButtonStep(1);
|
||||||
|
AddAssert("collection contains beatmap", () => collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo));
|
||||||
|
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
|
||||||
|
|
||||||
|
addClickAddOrRemoveButtonStep(1);
|
||||||
|
AddAssert("collection does not contain beatmap", () => !collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo));
|
||||||
|
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestManageCollectionsFilterIsNotSelected()
|
||||||
|
{
|
||||||
|
addExpandHeaderStep();
|
||||||
|
|
||||||
|
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
||||||
|
AddStep("select collection", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1));
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
addExpandHeaderStep();
|
||||||
|
|
||||||
|
AddStep("click manage collections filter", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(getCollectionDropdownItems().Last());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("collection filter still selected", () => control.CreateCriteria().Collection?.Name.Value == "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true)
|
||||||
|
=> AddAssert($"collection dropdown header displays '{collectionName}'",
|
||||||
|
() => shouldDisplay == (control.ChildrenOfType<CollectionFilterDropdown.CollectionDropdownHeader>().Single().ChildrenOfType<SpriteText>().First().Text == collectionName));
|
||||||
|
|
||||||
|
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
|
||||||
|
AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
|
||||||
|
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
|
||||||
|
() => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType<FillFlowContainer>().OfType<IHasText>().First().Text == collectionName)));
|
||||||
|
|
||||||
|
private IconButton getAddOrRemoveButton(int index)
|
||||||
|
=> getCollectionDropdownItems().ElementAt(index).ChildrenOfType<IconButton>().Single();
|
||||||
|
|
||||||
|
private void addExpandHeaderStep() => AddStep("expand header", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(control.ChildrenOfType<CollectionFilterDropdown.CollectionDropdownHeader>().Single());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(getAddOrRemoveButton(index));
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
private IEnumerable<Dropdown<CollectionMenuItem>.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems()
|
||||||
|
=> control.ChildrenOfType<CollectionFilterDropdown>().Single().ChildrenOfType<Dropdown<CollectionMenuItem>.DropdownMenu.DrawableDropdownMenuItem>();
|
||||||
|
}
|
||||||
|
}
|
47
osu.Game/Collections/BeatmapCollection.cs
Normal file
47
osu.Game/Collections/BeatmapCollection.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
|
||||||
|
namespace osu.Game.Collections
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A collection of beatmaps grouped by a name.
|
||||||
|
/// </summary>
|
||||||
|
public class BeatmapCollection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked whenever any change occurs on this <see cref="BeatmapCollection"/>.
|
||||||
|
/// </summary>
|
||||||
|
public event Action Changed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The collection's name.
|
||||||
|
/// </summary>
|
||||||
|
public readonly Bindable<string> Name = new Bindable<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The beatmaps contained by the collection.
|
||||||
|
/// </summary>
|
||||||
|
public readonly BindableList<BeatmapInfo> Beatmaps = new BindableList<BeatmapInfo>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date when this collection was last modified.
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset LastModifyDate { get; private set; } = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
public BeatmapCollection()
|
||||||
|
{
|
||||||
|
Beatmaps.CollectionChanged += (_, __) => onChange();
|
||||||
|
Name.ValueChanged += _ => onChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onChange()
|
||||||
|
{
|
||||||
|
LastModifyDate = DateTimeOffset.Now;
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
303
osu.Game/Collections/CollectionManager.cs
Normal file
303
osu.Game/Collections/CollectionManager.cs
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.IO.Legacy;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
|
||||||
|
namespace osu.Game.Collections
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles user-defined collections of beatmaps.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is currently reading and writing from the osu-stable file format. This is a temporary arrangement until we refactor the
|
||||||
|
/// database backing the game. Going forward writing should be done in a similar way to other model stores.
|
||||||
|
/// </remarks>
|
||||||
|
public class CollectionManager : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Database version in stable-compatible YYYYMMDD format.
|
||||||
|
/// </summary>
|
||||||
|
private const int database_version = 30000000;
|
||||||
|
|
||||||
|
private const string database_name = "collection.db";
|
||||||
|
|
||||||
|
public readonly BindableList<BeatmapCollection> Collections = new BindableList<BeatmapCollection>();
|
||||||
|
|
||||||
|
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private GameHost host { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapManager beatmaps { get; set; }
|
||||||
|
|
||||||
|
private readonly Storage storage;
|
||||||
|
|
||||||
|
public CollectionManager(Storage storage)
|
||||||
|
{
|
||||||
|
this.storage = storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Collections.CollectionChanged += collectionsChanged;
|
||||||
|
|
||||||
|
if (storage.Exists(database_name))
|
||||||
|
{
|
||||||
|
using (var stream = storage.GetStream(database_name))
|
||||||
|
importCollections(readCollections(stream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectionsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
foreach (var c in e.NewItems.Cast<BeatmapCollection>())
|
||||||
|
c.Changed += backgroundSave;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
foreach (var c in e.OldItems.Cast<BeatmapCollection>())
|
||||||
|
c.Changed -= backgroundSave;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotifyCollectionChangedAction.Replace:
|
||||||
|
foreach (var c in e.OldItems.Cast<BeatmapCollection>())
|
||||||
|
c.Changed -= backgroundSave;
|
||||||
|
|
||||||
|
foreach (var c in e.NewItems.Cast<BeatmapCollection>())
|
||||||
|
c.Changed += backgroundSave;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set an endpoint for notifications to be posted to.
|
||||||
|
/// </summary>
|
||||||
|
public Action<Notification> PostNotification { protected get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set a storage with access to an osu-stable install for import purposes.
|
||||||
|
/// </summary>
|
||||||
|
public Func<Storage> GetStableStorage { private get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
|
||||||
|
/// </summary>
|
||||||
|
public Task ImportFromStableAsync()
|
||||||
|
{
|
||||||
|
var stable = GetStableStorage?.Invoke();
|
||||||
|
|
||||||
|
if (stable == null)
|
||||||
|
{
|
||||||
|
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stable.Exists(database_name))
|
||||||
|
{
|
||||||
|
// This handles situations like when the user does not have a collections.db file
|
||||||
|
Logger.Log($"No {database_name} available in osu!stable installation", LoggingTarget.Information, LogLevel.Error);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using (var stream = stable.GetStream(database_name))
|
||||||
|
await Import(stream);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Import(Stream stream)
|
||||||
|
{
|
||||||
|
var notification = new ProgressNotification
|
||||||
|
{
|
||||||
|
State = ProgressNotificationState.Active,
|
||||||
|
Text = "Collections import is initialising..."
|
||||||
|
};
|
||||||
|
|
||||||
|
PostNotification?.Invoke(notification);
|
||||||
|
|
||||||
|
var collection = readCollections(stream, notification);
|
||||||
|
bool importCompleted = false;
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
importCollections(collection);
|
||||||
|
importCompleted = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
while (!IsDisposed && !importCompleted)
|
||||||
|
await Task.Delay(10);
|
||||||
|
|
||||||
|
notification.CompletionText = $"Imported {collection.Count} collections";
|
||||||
|
notification.State = ProgressNotificationState.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importCollections(List<BeatmapCollection> newCollections)
|
||||||
|
{
|
||||||
|
foreach (var newCol in newCollections)
|
||||||
|
{
|
||||||
|
var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name);
|
||||||
|
if (existing == null)
|
||||||
|
Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } });
|
||||||
|
|
||||||
|
foreach (var newBeatmap in newCol.Beatmaps)
|
||||||
|
{
|
||||||
|
if (!existing.Beatmaps.Contains(newBeatmap))
|
||||||
|
existing.Beatmaps.Add(newBeatmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BeatmapCollection> readCollections(Stream stream, ProgressNotification notification = null)
|
||||||
|
{
|
||||||
|
if (notification != null)
|
||||||
|
{
|
||||||
|
notification.Text = "Reading collections...";
|
||||||
|
notification.Progress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<BeatmapCollection>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var sr = new SerializationReader(stream))
|
||||||
|
{
|
||||||
|
sr.ReadInt32(); // Version
|
||||||
|
|
||||||
|
int collectionCount = sr.ReadInt32();
|
||||||
|
result.Capacity = collectionCount;
|
||||||
|
|
||||||
|
for (int i = 0; i < collectionCount; i++)
|
||||||
|
{
|
||||||
|
if (notification?.CancellationToken.IsCancellationRequested == true)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
var collection = new BeatmapCollection { Name = { Value = sr.ReadString() } };
|
||||||
|
int mapCount = sr.ReadInt32();
|
||||||
|
|
||||||
|
for (int j = 0; j < mapCount; j++)
|
||||||
|
{
|
||||||
|
if (notification?.CancellationToken.IsCancellationRequested == true)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
string checksum = sr.ReadString();
|
||||||
|
|
||||||
|
var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum);
|
||||||
|
if (beatmap != null)
|
||||||
|
collection.Beatmaps.Add(beatmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification != null)
|
||||||
|
{
|
||||||
|
notification.Text = $"Imported {i + 1} of {collectionCount} collections";
|
||||||
|
notification.Progress = (float)(i + 1) / collectionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, "Failed to read collection database.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteAll()
|
||||||
|
{
|
||||||
|
Collections.Clear();
|
||||||
|
PostNotification?.Invoke(new SimpleNotification { Text = "Deleted all collections!" });
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object saveLock = new object();
|
||||||
|
private int lastSave;
|
||||||
|
private int saveFailures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perform a save with debounce.
|
||||||
|
/// </summary>
|
||||||
|
private void backgroundSave()
|
||||||
|
{
|
||||||
|
var current = Interlocked.Increment(ref lastSave);
|
||||||
|
Task.Delay(100).ContinueWith(task =>
|
||||||
|
{
|
||||||
|
if (current != lastSave)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!save())
|
||||||
|
backgroundSave();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool save()
|
||||||
|
{
|
||||||
|
lock (saveLock)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref lastSave);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// This is NOT thread-safe!!
|
||||||
|
|
||||||
|
using (var sw = new SerializationWriter(storage.GetStream(database_name, FileAccess.Write)))
|
||||||
|
{
|
||||||
|
sw.Write(database_version);
|
||||||
|
sw.Write(Collections.Count);
|
||||||
|
|
||||||
|
foreach (var c in Collections)
|
||||||
|
{
|
||||||
|
sw.Write(c.Name.Value);
|
||||||
|
sw.Write(c.Beatmaps.Count);
|
||||||
|
|
||||||
|
foreach (var b in c.Beatmaps)
|
||||||
|
sw.Write(b.MD5Hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveFailures < 10)
|
||||||
|
saveFailures = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// Since this code is not thread-safe, we may run into random exceptions (such as collection enumeration or out of range indexing).
|
||||||
|
// Failures are thus only alerted if they exceed a threshold (once) to indicate "actual" errors having occurred.
|
||||||
|
if (++saveFailures == 10)
|
||||||
|
Logger.Error(e, "Failed to save collection database!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
osu.Game/Collections/DeleteCollectionDialog.cs
Normal file
34
osu.Game/Collections/DeleteCollectionDialog.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Humanizer;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Overlays.Dialog;
|
||||||
|
|
||||||
|
namespace osu.Game.Collections
|
||||||
|
{
|
||||||
|
public class DeleteCollectionDialog : PopupDialog
|
||||||
|
{
|
||||||
|
public DeleteCollectionDialog(BeatmapCollection collection, Action deleteAction)
|
||||||
|
{
|
||||||
|
HeaderText = "Confirm deletion of";
|
||||||
|
BodyText = $"{collection.Name.Value} ({"beatmap".ToQuantity(collection.Beatmaps.Count)})";
|
||||||
|
|
||||||
|
Icon = FontAwesome.Regular.TrashAlt;
|
||||||
|
|
||||||
|
Buttons = new PopupDialogButton[]
|
||||||
|
{
|
||||||
|
new PopupDialogOkButton
|
||||||
|
{
|
||||||
|
Text = @"Yes. Go for it.",
|
||||||
|
Action = deleteAction
|
||||||
|
},
|
||||||
|
new PopupDialogCancelButton
|
||||||
|
{
|
||||||
|
Text = @"No! Abort mission!",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
122
osu.Game/Collections/DrawableCollectionList.cs
Normal file
122
osu.Game/Collections/DrawableCollectionList.cs
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Collections
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Visualises a list of <see cref="BeatmapCollection"/>s.
|
||||||
|
/// </summary>
|
||||||
|
public class DrawableCollectionList : OsuRearrangeableListContainer<BeatmapCollection>
|
||||||
|
{
|
||||||
|
private Scroll scroll;
|
||||||
|
|
||||||
|
protected override ScrollContainer<Drawable> CreateScrollContainer() => scroll = new Scroll();
|
||||||
|
|
||||||
|
protected override FillFlowContainer<RearrangeableListItem<BeatmapCollection>> CreateListFillFlowContainer() => new Flow
|
||||||
|
{
|
||||||
|
DragActive = { BindTarget = DragActive }
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override OsuRearrangeableListItem<BeatmapCollection> CreateOsuDrawable(BeatmapCollection item)
|
||||||
|
{
|
||||||
|
if (item == scroll.PlaceholderItem.Model)
|
||||||
|
return scroll.ReplacePlaceholder();
|
||||||
|
|
||||||
|
return new DrawableCollectionListItem(item, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The scroll container for this <see cref="DrawableCollectionList"/>.
|
||||||
|
/// Contains the main flow of <see cref="DrawableCollectionListItem"/> and attaches a placeholder item to the end of the list.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Use <see cref="ReplacePlaceholder"/> to transfer the placeholder into the main list.
|
||||||
|
/// </remarks>
|
||||||
|
private class Scroll : OsuScrollContainer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The currently-displayed placeholder item.
|
||||||
|
/// </summary>
|
||||||
|
public DrawableCollectionListItem PlaceholderItem { get; private set; }
|
||||||
|
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
private readonly Container content;
|
||||||
|
|
||||||
|
private readonly Container<DrawableCollectionListItem> placeholderContainer;
|
||||||
|
|
||||||
|
public Scroll()
|
||||||
|
{
|
||||||
|
ScrollbarVisible = false;
|
||||||
|
Padding = new MarginPadding(10);
|
||||||
|
|
||||||
|
base.Content.Add(new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
LayoutDuration = 200,
|
||||||
|
LayoutEasing = Easing.OutQuint,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
content = new Container { RelativeSizeAxes = Axes.X },
|
||||||
|
placeholderContainer = new Container<DrawableCollectionListItem>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ReplacePlaceholder();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
// AutoSizeAxes cannot be used as the height should represent the post-layout-transform height at all times, so that the placeholder doesn't bounce around.
|
||||||
|
content.Height = ((Flow)Child).Children.Sum(c => c.DrawHeight + 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces the current <see cref="PlaceholderItem"/> with a new one, and returns the previous.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current <see cref="PlaceholderItem"/>.</returns>
|
||||||
|
public DrawableCollectionListItem ReplacePlaceholder()
|
||||||
|
{
|
||||||
|
var previous = PlaceholderItem;
|
||||||
|
|
||||||
|
placeholderContainer.Clear(false);
|
||||||
|
placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection(), false));
|
||||||
|
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The flow of <see cref="DrawableCollectionListItem"/>. Disables layout easing unless a drag is in progress.
|
||||||
|
/// </summary>
|
||||||
|
private class Flow : FillFlowContainer<RearrangeableListItem<BeatmapCollection>>
|
||||||
|
{
|
||||||
|
public readonly IBindable<bool> DragActive = new Bindable<bool>();
|
||||||
|
|
||||||
|
public Flow()
|
||||||
|
{
|
||||||
|
Spacing = new Vector2(0, 5);
|
||||||
|
LayoutEasing = Easing.OutQuint;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
DragActive.BindValueChanged(active => LayoutDuration = active.NewValue ? 200 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
237
osu.Game/Collections/DrawableCollectionListItem.cs
Normal file
237
osu.Game/Collections/DrawableCollectionListItem.cs
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Collections
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Visualises a <see cref="BeatmapCollection"/> inside a <see cref="DrawableCollectionList"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class DrawableCollectionListItem : OsuRearrangeableListItem<BeatmapCollection>
|
||||||
|
{
|
||||||
|
private const float item_height = 35;
|
||||||
|
private const float button_width = item_height * 0.75f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the <see cref="BeatmapCollection"/> currently exists inside the <see cref="CollectionManager"/>.
|
||||||
|
/// </summary>
|
||||||
|
public IBindable<bool> IsCreated => isCreated;
|
||||||
|
|
||||||
|
private readonly Bindable<bool> isCreated = new Bindable<bool>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="DrawableCollectionListItem"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The <see cref="BeatmapCollection"/>.</param>
|
||||||
|
/// <param name="isCreated">Whether <paramref name="item"/> currently exists inside the <see cref="CollectionManager"/>.</param>
|
||||||
|
public DrawableCollectionListItem(BeatmapCollection item, bool isCreated)
|
||||||
|
: base(item)
|
||||||
|
{
|
||||||
|
this.isCreated.Value = isCreated;
|
||||||
|
|
||||||
|
ShowDragHandle.BindTo(this.isCreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Drawable CreateContent() => new ItemContent(Model)
|
||||||
|
{
|
||||||
|
IsCreated = { BindTarget = isCreated }
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The main content of the <see cref="DrawableCollectionListItem"/>.
|
||||||
|
/// </summary>
|
||||||
|
private class ItemContent : CircularContainer
|
||||||
|
{
|
||||||
|
public readonly Bindable<bool> IsCreated = new Bindable<bool>();
|
||||||
|
|
||||||
|
private readonly IBindable<string> collectionName;
|
||||||
|
private readonly BeatmapCollection collection;
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private CollectionManager collectionManager { get; set; }
|
||||||
|
|
||||||
|
private Container textBoxPaddingContainer;
|
||||||
|
private ItemTextBox textBox;
|
||||||
|
|
||||||
|
public ItemContent(BeatmapCollection collection)
|
||||||
|
{
|
||||||
|
this.collection = collection;
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.X;
|
||||||
|
Height = item_height;
|
||||||
|
Masking = true;
|
||||||
|
|
||||||
|
collectionName = collection.Name.GetBoundCopy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new DeleteButton(collection)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
IsCreated = { BindTarget = IsCreated },
|
||||||
|
IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v)
|
||||||
|
},
|
||||||
|
textBoxPaddingContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Padding = new MarginPadding { Right = button_width },
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
textBox = new ItemTextBox
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Size = Vector2.One,
|
||||||
|
CornerRadius = item_height / 2,
|
||||||
|
Current = collection.Name,
|
||||||
|
PlaceholderText = IsCreated.Value ? string.Empty : "Create a new collection"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
collectionName.BindValueChanged(_ => createNewCollection(), true);
|
||||||
|
IsCreated.BindValueChanged(created => textBoxPaddingContainer.Padding = new MarginPadding { Right = created.NewValue ? button_width : 0 }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNewCollection()
|
||||||
|
{
|
||||||
|
if (IsCreated.Value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(collectionName.Value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Add the new collection and disable our placeholder. If all text is removed, the placeholder should not show back again.
|
||||||
|
collectionManager?.Collections.Add(collection);
|
||||||
|
textBox.PlaceholderText = string.Empty;
|
||||||
|
|
||||||
|
// When this item changes from placeholder to non-placeholder (via changing containers), its textbox will lose focus, so it needs to be re-focused.
|
||||||
|
Schedule(() => GetContainingInputManager().ChangeFocus(textBox));
|
||||||
|
|
||||||
|
IsCreated.Value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ItemTextBox : OsuTextBox
|
||||||
|
{
|
||||||
|
protected override float LeftRightPadding => item_height / 2;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
BackgroundUnfocused = colours.GreySeafoamDarker.Darken(0.5f);
|
||||||
|
BackgroundFocused = colours.GreySeafoam;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DeleteButton : CompositeDrawable
|
||||||
|
{
|
||||||
|
public readonly IBindable<bool> IsCreated = new Bindable<bool>();
|
||||||
|
|
||||||
|
public Func<Vector2, bool> IsTextBoxHovered;
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private DialogOverlay dialogOverlay { get; set; }
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private CollectionManager collectionManager { get; set; }
|
||||||
|
|
||||||
|
private readonly BeatmapCollection collection;
|
||||||
|
|
||||||
|
private Drawable fadeContainer;
|
||||||
|
private Drawable background;
|
||||||
|
|
||||||
|
public DeleteButton(BeatmapCollection collection)
|
||||||
|
{
|
||||||
|
this.collection = collection;
|
||||||
|
RelativeSizeAxes = Axes.Y;
|
||||||
|
|
||||||
|
Width = button_width + item_height / 2; // add corner radius to cover with fill
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
InternalChild = fadeContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Alpha = 0.1f,
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
background = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = colours.Red
|
||||||
|
},
|
||||||
|
new SpriteIcon
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
X = -button_width * 0.6f,
|
||||||
|
Size = new Vector2(10),
|
||||||
|
Icon = FontAwesome.Solid.Trash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
IsCreated.BindValueChanged(created => Alpha = created.NewValue ? 1 : 0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos);
|
||||||
|
|
||||||
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
{
|
||||||
|
fadeContainer.FadeTo(1f, 100, Easing.Out);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
|
{
|
||||||
|
fadeContainer.FadeTo(0.1f, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
{
|
||||||
|
background.FlashColour(Color4.White, 150);
|
||||||
|
|
||||||
|
if (collection.Beatmaps.Count == 0)
|
||||||
|
deleteCollection();
|
||||||
|
else
|
||||||
|
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteCollection() => collectionManager?.Collections.Remove(collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
134
osu.Game/Collections/ManageCollectionsDialog.cs
Normal file
134
osu.Game/Collections/ManageCollectionsDialog.cs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Collections
|
||||||
|
{
|
||||||
|
public class ManageCollectionsDialog : OsuFocusedOverlayContainer
|
||||||
|
{
|
||||||
|
private const double enter_duration = 500;
|
||||||
|
private const double exit_duration = 200;
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private CollectionManager collectionManager { get; set; }
|
||||||
|
|
||||||
|
public ManageCollectionsDialog()
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre;
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
Size = new Vector2(0.5f, 0.8f);
|
||||||
|
|
||||||
|
Masking = true;
|
||||||
|
CornerRadius = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = colours.GreySeafoamDark,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
RowDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Text = "Manage collections",
|
||||||
|
Font = OsuFont.GetFont(size: 30),
|
||||||
|
Padding = new MarginPadding { Vertical = 10 },
|
||||||
|
},
|
||||||
|
new IconButton
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Icon = FontAwesome.Solid.Times,
|
||||||
|
Colour = colours.GreySeafoamDarker,
|
||||||
|
Scale = new Vector2(0.8f),
|
||||||
|
X = -10,
|
||||||
|
Action = () => State.Value = Visibility.Hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = colours.GreySeafoamDarker
|
||||||
|
},
|
||||||
|
new DrawableCollectionList
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Items = { BindTarget = collectionManager?.Collections ?? new BindableList<BeatmapCollection>() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopIn()
|
||||||
|
{
|
||||||
|
base.PopIn();
|
||||||
|
|
||||||
|
this.FadeIn(enter_duration, Easing.OutQuint);
|
||||||
|
this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
base.PopOut();
|
||||||
|
|
||||||
|
this.FadeOut(exit_duration, Easing.OutQuint);
|
||||||
|
this.ScaleTo(0.9f, exit_duration);
|
||||||
|
|
||||||
|
// Ensure that textboxes commit
|
||||||
|
GetContainingInputManager()?.TriggerFocusContention(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,13 +12,13 @@ namespace osu.Game.Graphics.Containers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether any item is currently being dragged. Used to hide other items' drag handles.
|
/// Whether any item is currently being dragged. Used to hide other items' drag handles.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly BindableBool playlistDragActive = new BindableBool();
|
protected readonly BindableBool DragActive = new BindableBool();
|
||||||
|
|
||||||
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer();
|
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer();
|
||||||
|
|
||||||
protected sealed override RearrangeableListItem<TModel> CreateDrawable(TModel item) => CreateOsuDrawable(item).With(d =>
|
protected sealed override RearrangeableListItem<TModel> CreateDrawable(TModel item) => CreateOsuDrawable(item).With(d =>
|
||||||
{
|
{
|
||||||
d.PlaylistDragActive.BindTo(playlistDragActive);
|
d.DragActive.BindTo(DragActive);
|
||||||
});
|
});
|
||||||
|
|
||||||
protected abstract OsuRearrangeableListItem<TModel> CreateOsuDrawable(TModel item);
|
protected abstract OsuRearrangeableListItem<TModel> CreateOsuDrawable(TModel item);
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether any item is currently being dragged. Used to hide other items' drag handles.
|
/// Whether any item is currently being dragged. Used to hide other items' drag handles.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly BindableBool PlaylistDragActive = new BindableBool();
|
public readonly BindableBool DragActive = new BindableBool();
|
||||||
|
|
||||||
private Color4 handleColour = Color4.White;
|
private Color4 handleColour = Color4.White;
|
||||||
|
|
||||||
@ -44,8 +44,9 @@ namespace osu.Game.Graphics.Containers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the drag handle should be shown.
|
/// Whether the drag handle should be shown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual bool ShowDragHandle => true;
|
protected readonly Bindable<bool> ShowDragHandle = new Bindable<bool>();
|
||||||
|
|
||||||
|
private Container handleContainer;
|
||||||
private PlaylistItemHandle handle;
|
private PlaylistItemHandle handle;
|
||||||
|
|
||||||
protected OsuRearrangeableListItem(TModel item)
|
protected OsuRearrangeableListItem(TModel item)
|
||||||
@ -58,8 +59,6 @@ namespace osu.Game.Graphics.Containers
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
Container handleContainer;
|
|
||||||
|
|
||||||
InternalChild = new GridContainer
|
InternalChild = new GridContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
@ -88,9 +87,12 @@ namespace osu.Game.Graphics.Containers
|
|||||||
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
|
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!ShowDragHandle)
|
protected override void LoadComplete()
|
||||||
handleContainer.Alpha = 0;
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
ShowDragHandle.BindValueChanged(show => handleContainer.Alpha = show.NewValue ? 1 : 0, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnDragStart(DragStartEvent e)
|
protected override bool OnDragStart(DragStartEvent e)
|
||||||
@ -98,13 +100,13 @@ namespace osu.Game.Graphics.Containers
|
|||||||
if (!base.OnDragStart(e))
|
if (!base.OnDragStart(e))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
PlaylistDragActive.Value = true;
|
DragActive.Value = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDragEnd(DragEndEvent e)
|
protected override void OnDragEnd(DragEndEvent e)
|
||||||
{
|
{
|
||||||
PlaylistDragActive.Value = false;
|
DragActive.Value = false;
|
||||||
base.OnDragEnd(e);
|
base.OnDragEnd(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +114,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
{
|
{
|
||||||
handle.UpdateHoverState(IsDragged || !PlaylistDragActive.Value);
|
handle.UpdateHoverState(IsDragged || !DragActive.Value);
|
||||||
return base.OnHover(e);
|
return base.OnHover(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
iconColour = value;
|
iconColour = value;
|
||||||
icon.Colour = value;
|
icon.FadeColour(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,8 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
};
|
};
|
||||||
|
|
||||||
ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL };
|
ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL };
|
||||||
|
|
||||||
|
MaxHeight = 250;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
|
@ -31,6 +31,7 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Collections;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
@ -610,12 +611,19 @@ namespace osu.Game
|
|||||||
d.Origin = Anchor.TopRight;
|
d.Origin = Anchor.TopRight;
|
||||||
}), rightFloatingOverlayContent.Add, true);
|
}), rightFloatingOverlayContent.Add, true);
|
||||||
|
|
||||||
|
loadComponentSingleFile(new CollectionManager(Storage)
|
||||||
|
{
|
||||||
|
PostNotification = n => notifications.Post(n),
|
||||||
|
GetStableStorage = GetStorageForStableInstall
|
||||||
|
}, Add, true);
|
||||||
|
|
||||||
loadComponentSingleFile(screenshotManager, Add);
|
loadComponentSingleFile(screenshotManager, Add);
|
||||||
|
|
||||||
// dependency on notification overlay, dependent by settings overlay
|
// dependency on notification overlay, dependent by settings overlay
|
||||||
loadComponentSingleFile(CreateUpdateManager(), Add, true);
|
loadComponentSingleFile(CreateUpdateManager(), Add, true);
|
||||||
|
|
||||||
// overlay elements
|
// overlay elements
|
||||||
|
loadComponentSingleFile(new ManageCollectionsDialog(), overlayContent.Add, true);
|
||||||
loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true);
|
loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true);
|
||||||
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
|
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
|
||||||
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
|
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Collections;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -19,14 +21,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
|||||||
private TriangleButton importBeatmapsButton;
|
private TriangleButton importBeatmapsButton;
|
||||||
private TriangleButton importScoresButton;
|
private TriangleButton importScoresButton;
|
||||||
private TriangleButton importSkinsButton;
|
private TriangleButton importSkinsButton;
|
||||||
|
private TriangleButton importCollectionsButton;
|
||||||
private TriangleButton deleteBeatmapsButton;
|
private TriangleButton deleteBeatmapsButton;
|
||||||
private TriangleButton deleteScoresButton;
|
private TriangleButton deleteScoresButton;
|
||||||
private TriangleButton deleteSkinsButton;
|
private TriangleButton deleteSkinsButton;
|
||||||
private TriangleButton restoreButton;
|
private TriangleButton restoreButton;
|
||||||
private TriangleButton undeleteButton;
|
private TriangleButton undeleteButton;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader(permitNulls: true)]
|
||||||
private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay)
|
private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, DialogOverlay dialogOverlay)
|
||||||
{
|
{
|
||||||
if (beatmaps.SupportsImportFromStable)
|
if (beatmaps.SupportsImportFromStable)
|
||||||
{
|
{
|
||||||
@ -93,20 +96,46 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
AddRange(new Drawable[]
|
Add(deleteSkinsButton = new DangerousSettingsButton
|
||||||
{
|
{
|
||||||
deleteSkinsButton = new DangerousSettingsButton
|
Text = "Delete ALL skins",
|
||||||
|
Action = () =>
|
||||||
{
|
{
|
||||||
Text = "Delete ALL skins",
|
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() =>
|
||||||
|
{
|
||||||
|
deleteSkinsButton.Enabled.Value = false;
|
||||||
|
Task.Run(() => skins.Delete(skins.GetAllUserSkins())).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (collectionManager != null)
|
||||||
|
{
|
||||||
|
if (collectionManager.SupportsImportFromStable)
|
||||||
|
{
|
||||||
|
Add(importCollectionsButton = new SettingsButton
|
||||||
|
{
|
||||||
|
Text = "Import collections from stable",
|
||||||
|
Action = () =>
|
||||||
|
{
|
||||||
|
importCollectionsButton.Enabled.Value = false;
|
||||||
|
collectionManager.ImportFromStableAsync().ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Add(new DangerousSettingsButton
|
||||||
|
{
|
||||||
|
Text = "Delete ALL collections",
|
||||||
Action = () =>
|
Action = () =>
|
||||||
{
|
{
|
||||||
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() =>
|
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(collectionManager.DeleteAll));
|
||||||
{
|
|
||||||
deleteSkinsButton.Enabled.Value = false;
|
|
||||||
Task.Run(() => skins.Delete(skins.GetAllUserSkins())).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true));
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
AddRange(new Drawable[]
|
||||||
|
{
|
||||||
restoreButton = new SettingsButton
|
restoreButton = new SettingsButton
|
||||||
{
|
{
|
||||||
Text = "Restore all hidden difficulties",
|
Text = "Restore all hidden difficulties",
|
||||||
|
@ -37,8 +37,6 @@ namespace osu.Game.Screens.Multi
|
|||||||
|
|
||||||
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
|
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
|
||||||
|
|
||||||
protected override bool ShowDragHandle => allowEdit;
|
|
||||||
|
|
||||||
private Container maskingContainer;
|
private Container maskingContainer;
|
||||||
private Container difficultyIconContainer;
|
private Container difficultyIconContainer;
|
||||||
private LinkFlowContainer beatmapText;
|
private LinkFlowContainer beatmapText;
|
||||||
@ -63,12 +61,13 @@ namespace osu.Game.Screens.Multi
|
|||||||
|
|
||||||
// TODO: edit support should be moved out into a derived class
|
// TODO: edit support should be moved out into a derived class
|
||||||
this.allowEdit = allowEdit;
|
this.allowEdit = allowEdit;
|
||||||
|
|
||||||
this.allowSelection = allowSelection;
|
this.allowSelection = allowSelection;
|
||||||
|
|
||||||
beatmap.BindTo(item.Beatmap);
|
beatmap.BindTo(item.Beatmap);
|
||||||
ruleset.BindTo(item.Ruleset);
|
ruleset.BindTo(item.Ruleset);
|
||||||
requiredMods.BindTo(item.RequiredMods);
|
requiredMods.BindTo(item.RequiredMods);
|
||||||
|
|
||||||
|
ShowDragHandle.Value = allowEdit;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
|
@ -60,6 +60,9 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0);
|
match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (match)
|
||||||
|
match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true;
|
||||||
|
|
||||||
Filtered.Value = !match;
|
Filtered.Value = !match;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -17,6 +18,7 @@ using osu.Framework.Graphics.UserInterface;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.Drawables;
|
using osu.Game.Beatmaps.Drawables;
|
||||||
|
using osu.Game.Collections;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Backgrounds;
|
using osu.Game.Graphics.Backgrounds;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
@ -46,6 +48,12 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private BeatmapDifficultyManager difficultyManager { get; set; }
|
private BeatmapDifficultyManager difficultyManager { get; set; }
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private CollectionManager collectionManager { get; set; }
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
||||||
|
|
||||||
private IBindable<StarDifficulty> starDifficultyBindable;
|
private IBindable<StarDifficulty> starDifficultyBindable;
|
||||||
private CancellationTokenSource starDifficultyCancellationSource;
|
private CancellationTokenSource starDifficultyCancellationSource;
|
||||||
|
|
||||||
@ -213,16 +221,39 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
if (editRequested != null)
|
if (editRequested != null)
|
||||||
items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmap)));
|
items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmap)));
|
||||||
|
|
||||||
|
if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null)
|
||||||
|
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value)));
|
||||||
|
|
||||||
|
if (collectionManager != null)
|
||||||
|
{
|
||||||
|
var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList();
|
||||||
|
if (manageCollectionsDialog != null)
|
||||||
|
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||||
|
|
||||||
|
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
|
||||||
|
}
|
||||||
|
|
||||||
if (hideRequested != null)
|
if (hideRequested != null)
|
||||||
items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmap)));
|
items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmap)));
|
||||||
|
|
||||||
if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null)
|
|
||||||
items.Add(new OsuMenuItem("Details", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value)));
|
|
||||||
|
|
||||||
return items.ToArray();
|
return items.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MenuItem createCollectionMenuItem(BeatmapCollection collection)
|
||||||
|
{
|
||||||
|
return new ToggleMenuItem(collection.Name.Value, MenuItemType.Standard, s =>
|
||||||
|
{
|
||||||
|
if (s)
|
||||||
|
collection.Beatmaps.Add(beatmap);
|
||||||
|
else
|
||||||
|
collection.Beatmaps.Remove(beatmap);
|
||||||
|
})
|
||||||
|
{
|
||||||
|
State = { Value = collection.Beatmaps.Contains(beatmap) }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
@ -16,6 +16,7 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.Drawables;
|
using osu.Game.Beatmaps.Drawables;
|
||||||
|
using osu.Game.Collections;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
@ -34,6 +35,12 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private DialogOverlay dialogOverlay { get; set; }
|
private DialogOverlay dialogOverlay { get; set; }
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private CollectionManager collectionManager { get; set; }
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
||||||
|
|
||||||
private readonly BeatmapSetInfo beatmapSet;
|
private readonly BeatmapSetInfo beatmapSet;
|
||||||
|
|
||||||
public DrawableCarouselBeatmapSet(CarouselBeatmapSet set)
|
public DrawableCarouselBeatmapSet(CarouselBeatmapSet set)
|
||||||
@ -135,16 +142,61 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
if (beatmapSet.OnlineBeatmapSetID != null && viewDetails != null)
|
if (beatmapSet.OnlineBeatmapSetID != null && viewDetails != null)
|
||||||
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineBeatmapSetID.Value)));
|
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineBeatmapSetID.Value)));
|
||||||
|
|
||||||
|
if (collectionManager != null)
|
||||||
|
{
|
||||||
|
var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList();
|
||||||
|
if (manageCollectionsDialog != null)
|
||||||
|
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||||
|
|
||||||
|
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
|
||||||
|
}
|
||||||
|
|
||||||
if (beatmapSet.Beatmaps.Any(b => b.Hidden))
|
if (beatmapSet.Beatmaps.Any(b => b.Hidden))
|
||||||
items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet)));
|
items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet)));
|
||||||
|
|
||||||
if (dialogOverlay != null)
|
if (dialogOverlay != null)
|
||||||
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet))));
|
items.Add(new OsuMenuItem("Delete...", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet))));
|
||||||
|
|
||||||
return items.ToArray();
|
return items.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MenuItem createCollectionMenuItem(BeatmapCollection collection)
|
||||||
|
{
|
||||||
|
TernaryState state;
|
||||||
|
|
||||||
|
var countExisting = beatmapSet.Beatmaps.Count(b => collection.Beatmaps.Contains(b));
|
||||||
|
|
||||||
|
if (countExisting == beatmapSet.Beatmaps.Count)
|
||||||
|
state = TernaryState.True;
|
||||||
|
else if (countExisting > 0)
|
||||||
|
state = TernaryState.Indeterminate;
|
||||||
|
else
|
||||||
|
state = TernaryState.False;
|
||||||
|
|
||||||
|
return new TernaryStateMenuItem(collection.Name.Value, MenuItemType.Standard, s =>
|
||||||
|
{
|
||||||
|
foreach (var b in beatmapSet.Beatmaps)
|
||||||
|
{
|
||||||
|
switch (s)
|
||||||
|
{
|
||||||
|
case TernaryState.True:
|
||||||
|
if (collection.Beatmaps.Contains(b))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
collection.Beatmaps.Add(b);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TernaryState.False:
|
||||||
|
collection.Beatmaps.Remove(b);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
State = { Value = state }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private class PanelBackground : BufferedContainer
|
private class PanelBackground : BufferedContainer
|
||||||
{
|
{
|
||||||
public PanelBackground(WorkingBeatmap working)
|
public PanelBackground(WorkingBeatmap working)
|
||||||
|
273
osu.Game/Screens/Select/CollectionFilterDropdown.cs
Normal file
273
osu.Game/Screens/Select/CollectionFilterDropdown.cs
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Collections;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Select
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A dropdown to select the <see cref="CollectionMenuItem"/> to filter beatmaps using.
|
||||||
|
/// </summary>
|
||||||
|
public class CollectionFilterDropdown : OsuDropdown<CollectionMenuItem>
|
||||||
|
{
|
||||||
|
private readonly IBindableList<BeatmapCollection> collections = new BindableList<BeatmapCollection>();
|
||||||
|
private readonly IBindableList<BeatmapInfo> beatmaps = new BindableList<BeatmapInfo>();
|
||||||
|
private readonly BindableList<CollectionMenuItem> filters = new BindableList<CollectionMenuItem>();
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
||||||
|
|
||||||
|
public CollectionFilterDropdown()
|
||||||
|
{
|
||||||
|
ItemSource = filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader(permitNulls: true)]
|
||||||
|
private void load([CanBeNull] CollectionManager collectionManager)
|
||||||
|
{
|
||||||
|
if (collectionManager != null)
|
||||||
|
collections.BindTo(collectionManager.Collections);
|
||||||
|
|
||||||
|
collections.CollectionChanged += (_, __) => collectionsChanged();
|
||||||
|
collectionsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
Current.BindValueChanged(filterChanged, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when a collection has been added or removed.
|
||||||
|
/// </summary>
|
||||||
|
private void collectionsChanged()
|
||||||
|
{
|
||||||
|
var selectedItem = SelectedItem?.Value?.Collection;
|
||||||
|
|
||||||
|
filters.Clear();
|
||||||
|
filters.Add(new AllBeatmapsCollectionMenuItem());
|
||||||
|
filters.AddRange(collections.Select(c => new CollectionMenuItem(c)));
|
||||||
|
filters.Add(new ManageCollectionsMenuItem());
|
||||||
|
|
||||||
|
Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection == selectedItem) ?? filters[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when the <see cref="CollectionMenuItem"/> selection has changed.
|
||||||
|
/// </summary>
|
||||||
|
private void filterChanged(ValueChangedEvent<CollectionMenuItem> filter)
|
||||||
|
{
|
||||||
|
// Binding the beatmaps will trigger a collection change event, which results in an infinite-loop. This is rebound later, when it's safe to do so.
|
||||||
|
beatmaps.CollectionChanged -= filterBeatmapsChanged;
|
||||||
|
|
||||||
|
if (filter.OldValue?.Collection != null)
|
||||||
|
beatmaps.UnbindFrom(filter.OldValue.Collection.Beatmaps);
|
||||||
|
|
||||||
|
if (filter.NewValue?.Collection != null)
|
||||||
|
beatmaps.BindTo(filter.NewValue.Collection.Beatmaps);
|
||||||
|
|
||||||
|
beatmaps.CollectionChanged += filterBeatmapsChanged;
|
||||||
|
|
||||||
|
// Never select the manage collection filter - rollback to the previous filter.
|
||||||
|
// This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value.
|
||||||
|
if (filter.NewValue is ManageCollectionsMenuItem)
|
||||||
|
{
|
||||||
|
Current.Value = filter.OldValue;
|
||||||
|
manageCollectionsDialog?.Show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when the beatmaps contained by a <see cref="BeatmapCollection"/> have changed.
|
||||||
|
/// </summary>
|
||||||
|
private void filterBeatmapsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
// The filtered beatmaps have changed, without the filter having changed itself. So a change in filter must be notified.
|
||||||
|
// Note that this does NOT propagate to bound bindables, so the FilterControl must bind directly to the value change event of this bindable.
|
||||||
|
Current.TriggerChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GenerateItemText(CollectionMenuItem item) => item.CollectionName.Value;
|
||||||
|
|
||||||
|
protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader
|
||||||
|
{
|
||||||
|
SelectedItem = { BindTarget = Current }
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override DropdownMenu CreateMenu() => new CollectionDropdownMenu();
|
||||||
|
|
||||||
|
public class CollectionDropdownHeader : OsuDropdownHeader
|
||||||
|
{
|
||||||
|
public readonly Bindable<CollectionMenuItem> SelectedItem = new Bindable<CollectionMenuItem>();
|
||||||
|
private readonly Bindable<string> collectionName = new Bindable<string>();
|
||||||
|
|
||||||
|
protected override string Label
|
||||||
|
{
|
||||||
|
get => base.Label;
|
||||||
|
set { } // See updateText().
|
||||||
|
}
|
||||||
|
|
||||||
|
public CollectionDropdownHeader()
|
||||||
|
{
|
||||||
|
Height = 25;
|
||||||
|
Icon.Size = new Vector2(16);
|
||||||
|
Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
SelectedItem.BindValueChanged(_ => updateBindable(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBindable()
|
||||||
|
{
|
||||||
|
collectionName.UnbindAll();
|
||||||
|
|
||||||
|
if (SelectedItem.Value != null)
|
||||||
|
collectionName.BindTo(SelectedItem.Value.CollectionName);
|
||||||
|
|
||||||
|
collectionName.BindValueChanged(_ => updateText(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropdowns don't bind to value changes, so the real name is copied directly from the selected item here.
|
||||||
|
private void updateText() => base.Label = collectionName.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CollectionDropdownMenu : OsuDropdownMenu
|
||||||
|
{
|
||||||
|
public CollectionDropdownMenu()
|
||||||
|
{
|
||||||
|
MaxHeight = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem
|
||||||
|
{
|
||||||
|
[NotNull]
|
||||||
|
protected new CollectionMenuItem Item => ((DropdownMenuItem<CollectionMenuItem>)base.Item).Value;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IBindable<WorkingBeatmap> beatmap { get; set; }
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
private readonly BindableList<BeatmapInfo> collectionBeatmaps;
|
||||||
|
|
||||||
|
[NotNull]
|
||||||
|
private readonly Bindable<string> collectionName;
|
||||||
|
|
||||||
|
private IconButton addOrRemoveButton;
|
||||||
|
private Content content;
|
||||||
|
private bool beatmapInCollection;
|
||||||
|
|
||||||
|
public CollectionDropdownMenuItem(MenuItem item)
|
||||||
|
: base(item)
|
||||||
|
{
|
||||||
|
collectionBeatmaps = Item.Collection?.Beatmaps.GetBoundCopy();
|
||||||
|
collectionName = Item.CollectionName.GetBoundCopy();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
AddInternal(addOrRemoveButton = new IconButton
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
X = -OsuScrollContainer.SCROLL_BAR_HEIGHT,
|
||||||
|
Scale = new Vector2(0.65f),
|
||||||
|
Action = addOrRemove,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
if (collectionBeatmaps != null)
|
||||||
|
{
|
||||||
|
collectionBeatmaps.CollectionChanged += (_, __) => collectionChanged();
|
||||||
|
beatmap.BindValueChanged(_ => collectionChanged(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Although the DrawableMenuItem binds to value changes of the item's text, the item is an internal implementation detail of Dropdown that has no knowledge
|
||||||
|
// of the underlying CollectionFilter value and its accompanying name, so the real name has to be copied here. Without this, the collection name wouldn't update when changed.
|
||||||
|
collectionName.BindValueChanged(name => content.Text = name.NewValue, true);
|
||||||
|
|
||||||
|
updateButtonVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
{
|
||||||
|
updateButtonVisibility();
|
||||||
|
return base.OnHover(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
|
{
|
||||||
|
updateButtonVisibility();
|
||||||
|
base.OnHoverLost(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectionChanged()
|
||||||
|
{
|
||||||
|
Debug.Assert(collectionBeatmaps != null);
|
||||||
|
|
||||||
|
beatmapInCollection = collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo);
|
||||||
|
|
||||||
|
addOrRemoveButton.Enabled.Value = !beatmap.IsDefault;
|
||||||
|
addOrRemoveButton.Icon = beatmapInCollection ? FontAwesome.Solid.MinusSquare : FontAwesome.Solid.PlusSquare;
|
||||||
|
addOrRemoveButton.TooltipText = beatmapInCollection ? "Remove selected beatmap" : "Add selected beatmap";
|
||||||
|
|
||||||
|
updateButtonVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSelectChange()
|
||||||
|
{
|
||||||
|
base.OnSelectChange();
|
||||||
|
updateButtonVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateButtonVisibility()
|
||||||
|
{
|
||||||
|
if (collectionBeatmaps == null)
|
||||||
|
addOrRemoveButton.Alpha = 0;
|
||||||
|
else
|
||||||
|
addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addOrRemove()
|
||||||
|
{
|
||||||
|
Debug.Assert(collectionBeatmaps != null);
|
||||||
|
|
||||||
|
if (!collectionBeatmaps.Remove(beatmap.Value.BeatmapInfo))
|
||||||
|
collectionBeatmaps.Add(beatmap.Value.BeatmapInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Drawable CreateContent() => content = (Content)base.CreateContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
osu.Game/Screens/Select/CollectionMenuItem.cs
Normal file
55
osu.Game/Screens/Select/CollectionMenuItem.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Collections;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Select
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="BeatmapCollection"/> filter.
|
||||||
|
/// </summary>
|
||||||
|
public class CollectionMenuItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The collection to filter beatmaps from.
|
||||||
|
/// May be null to not filter by collection (include all beatmaps).
|
||||||
|
/// </summary>
|
||||||
|
[CanBeNull]
|
||||||
|
public readonly BeatmapCollection Collection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the collection.
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public readonly Bindable<string> CollectionName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="CollectionMenuItem"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection">The collection to filter beatmaps from.</param>
|
||||||
|
public CollectionMenuItem([CanBeNull] BeatmapCollection collection)
|
||||||
|
{
|
||||||
|
Collection = collection;
|
||||||
|
CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable<string>("All beatmaps");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AllBeatmapsCollectionMenuItem : CollectionMenuItem
|
||||||
|
{
|
||||||
|
public AllBeatmapsCollectionMenuItem()
|
||||||
|
: base(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ManageCollectionsMenuItem : CollectionMenuItem
|
||||||
|
{
|
||||||
|
public ManageCollectionsMenuItem()
|
||||||
|
: base(null)
|
||||||
|
{
|
||||||
|
CollectionName.Value = "Manage collections...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,8 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
public class FilterControl : Container
|
public class FilterControl : Container
|
||||||
{
|
{
|
||||||
public const float HEIGHT = 100;
|
public const float HEIGHT = 2 * side_margin + 85;
|
||||||
|
private const float side_margin = 20;
|
||||||
|
|
||||||
public Action<FilterCriteria> FilterChanged;
|
public Action<FilterCriteria> FilterChanged;
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ namespace osu.Game.Screens.Select
|
|||||||
Sort = sortMode.Value,
|
Sort = sortMode.Value,
|
||||||
AllowConvertedBeatmaps = showConverted.Value,
|
AllowConvertedBeatmaps = showConverted.Value,
|
||||||
Ruleset = ruleset.Value,
|
Ruleset = ruleset.Value,
|
||||||
|
Collection = collectionDropdown?.Current.Value.Collection
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!minimumStars.IsDefault)
|
if (!minimumStars.IsDefault)
|
||||||
@ -54,6 +56,7 @@ namespace osu.Game.Screens.Select
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SeekLimitedSearchTextBox searchTextBox;
|
private SeekLimitedSearchTextBox searchTextBox;
|
||||||
|
private CollectionFilterDropdown collectionDropdown;
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||||
base.ReceivePositionalInputAt(screenSpacePos) || sortTabs.ReceivePositionalInputAt(screenSpacePos);
|
base.ReceivePositionalInputAt(screenSpacePos) || sortTabs.ReceivePositionalInputAt(screenSpacePos);
|
||||||
@ -90,65 +93,104 @@ namespace osu.Game.Screens.Select
|
|||||||
},
|
},
|
||||||
new Container
|
new Container
|
||||||
{
|
{
|
||||||
Padding = new MarginPadding(20),
|
Padding = new MarginPadding(side_margin),
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 0.5f,
|
Width = 0.5f,
|
||||||
Anchor = Anchor.TopRight,
|
Anchor = Anchor.TopRight,
|
||||||
Origin = Anchor.TopRight,
|
Origin = Anchor.TopRight,
|
||||||
Children = new Drawable[]
|
Child = new GridContainer
|
||||||
{
|
{
|
||||||
searchTextBox = new SeekLimitedSearchTextBox { RelativeSizeAxes = Axes.X },
|
RelativeSizeAxes = Axes.Both,
|
||||||
new Box
|
RowDimensions = new[]
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
new Dimension(GridSizeMode.Absolute, 60),
|
||||||
Height = 1,
|
new Dimension(GridSizeMode.Absolute, 5),
|
||||||
Colour = OsuColour.Gray(80),
|
new Dimension(GridSizeMode.Absolute, 20),
|
||||||
Origin = Anchor.BottomLeft,
|
|
||||||
Anchor = Anchor.BottomLeft,
|
|
||||||
},
|
},
|
||||||
new FillFlowContainer
|
Content = new[]
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomRight,
|
new Drawable[]
|
||||||
Origin = Anchor.BottomRight,
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Spacing = new Vector2(OsuTabControl<SortMode>.HORIZONTAL_SPACING, 0),
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
{
|
||||||
new OsuTabControlCheckbox
|
new Container
|
||||||
{
|
{
|
||||||
Text = "Show converted",
|
RelativeSizeAxes = Axes.Both,
|
||||||
Current = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps),
|
Children = new Drawable[]
|
||||||
Anchor = Anchor.BottomRight,
|
{
|
||||||
Origin = Anchor.BottomRight,
|
searchTextBox = new SeekLimitedSearchTextBox { RelativeSizeAxes = Axes.X },
|
||||||
},
|
new Box
|
||||||
sortTabs = new OsuTabControl<SortMode>
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Height = 1,
|
||||||
|
Colour = OsuColour.Gray(80),
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomRight,
|
||||||
|
Origin = Anchor.BottomRight,
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(OsuTabControl<SortMode>.HORIZONTAL_SPACING, 0),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuTabControlCheckbox
|
||||||
|
{
|
||||||
|
Text = "Show converted",
|
||||||
|
Current = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps),
|
||||||
|
Anchor = Anchor.BottomRight,
|
||||||
|
Origin = Anchor.BottomRight,
|
||||||
|
},
|
||||||
|
sortTabs = new OsuTabControl<SortMode>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 0.5f,
|
||||||
|
Height = 24,
|
||||||
|
AutoSort = true,
|
||||||
|
Anchor = Anchor.BottomRight,
|
||||||
|
Origin = Anchor.BottomRight,
|
||||||
|
AccentColour = colours.GreenLight,
|
||||||
|
Current = { BindTarget = sortMode }
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Text = "Sort by",
|
||||||
|
Font = OsuFont.GetFont(size: 14),
|
||||||
|
Margin = new MarginPadding(5),
|
||||||
|
Anchor = Anchor.BottomRight,
|
||||||
|
Origin = Anchor.BottomRight,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 0.5f,
|
Children = new Drawable[]
|
||||||
Height = 24,
|
{
|
||||||
AutoSort = true,
|
collectionDropdown = new CollectionFilterDropdown
|
||||||
Anchor = Anchor.BottomRight,
|
{
|
||||||
Origin = Anchor.BottomRight,
|
Anchor = Anchor.TopRight,
|
||||||
AccentColour = colours.GreenLight,
|
Origin = Anchor.TopRight,
|
||||||
Current = { BindTarget = sortMode }
|
RelativeSizeAxes = Axes.X,
|
||||||
},
|
Width = 0.4f,
|
||||||
new OsuSpriteText
|
}
|
||||||
{
|
}
|
||||||
Text = "Sort by",
|
}
|
||||||
Font = OsuFont.GetFont(size: 14),
|
},
|
||||||
Margin = new MarginPadding(5),
|
}
|
||||||
Anchor = Anchor.BottomRight,
|
|
||||||
Origin = Anchor.BottomRight,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
searchTextBox.Current.ValueChanged += _ => FilterChanged?.Invoke(CreateCriteria());
|
collectionDropdown.Current.ValueChanged += _ => updateCriteria();
|
||||||
|
searchTextBox.Current.ValueChanged += _ => updateCriteria();
|
||||||
|
|
||||||
updateCriteria();
|
updateCriteria();
|
||||||
}
|
}
|
||||||
@ -156,7 +198,6 @@ namespace osu.Game.Screens.Select
|
|||||||
public void Deactivate()
|
public void Deactivate()
|
||||||
{
|
{
|
||||||
searchTextBox.ReadOnly = true;
|
searchTextBox.ReadOnly = true;
|
||||||
|
|
||||||
searchTextBox.HoldFocus = false;
|
searchTextBox.HoldFocus = false;
|
||||||
if (searchTextBox.HasFocus)
|
if (searchTextBox.HasFocus)
|
||||||
GetContainingInputManager().ChangeFocus(searchTextBox);
|
GetContainingInputManager().ChangeFocus(searchTextBox);
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Collections;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Screens.Select.Filter;
|
using osu.Game.Screens.Select.Filter;
|
||||||
|
|
||||||
@ -51,6 +53,12 @@ namespace osu.Game.Screens.Select
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The collection to filter beatmaps from.
|
||||||
|
/// </summary>
|
||||||
|
[CanBeNull]
|
||||||
|
public BeatmapCollection Collection;
|
||||||
|
|
||||||
public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
|
public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
|
||||||
where T : struct
|
where T : struct
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user