mirror of
https://github.com/ppy/osu.git
synced 2025-02-15 17:23:52 +08:00
Merge pull request #19430 from peppy/realm-collections
Move beatmap collections to realm
This commit is contained in:
commit
5003eb5629
@ -5,12 +5,15 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Collections;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Collections.IO
|
namespace osu.Game.Tests.Collections.IO
|
||||||
@ -29,7 +32,11 @@ namespace osu.Game.Tests.Collections.IO
|
|||||||
|
|
||||||
await importCollectionsFromStream(osu, new MemoryStream());
|
await importCollectionsFromStream(osu, new MemoryStream());
|
||||||
|
|
||||||
Assert.That(osu.CollectionManager.Collections.Count, Is.Zero);
|
osu.Realm.Run(realm =>
|
||||||
|
{
|
||||||
|
var collections = realm.All<BeatmapCollection>().ToList();
|
||||||
|
Assert.That(collections.Count, Is.Zero);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -49,18 +56,22 @@ namespace osu.Game.Tests.Collections.IO
|
|||||||
|
|
||||||
await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
|
await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
|
||||||
|
|
||||||
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
|
osu.Realm.Run(realm =>
|
||||||
|
{
|
||||||
|
var collections = realm.All<BeatmapCollection>().ToList();
|
||||||
|
Assert.That(collections.Count, Is.EqualTo(2));
|
||||||
|
|
||||||
// Even with no beatmaps imported, collections are tracking the hashes and will continue to.
|
// Even with no beatmaps imported, collections are tracking the hashes and will continue to.
|
||||||
// In the future this whole mechanism will be replaced with having the collections in realm,
|
// In the future this whole mechanism will be replaced with having the collections in realm,
|
||||||
// but until that happens it makes rough sense that we want to track not-yet-imported beatmaps
|
// but until that happens it makes rough sense that we want to track not-yet-imported beatmaps
|
||||||
// and have them associate with collections if/when they become available.
|
// and have them associate with collections if/when they become available.
|
||||||
|
|
||||||
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
|
Assert.That(collections[0].Name, Is.EqualTo("First"));
|
||||||
Assert.That(osu.CollectionManager.Collections[0].BeatmapHashes.Count, Is.EqualTo(1));
|
Assert.That(collections[0].BeatmapMD5Hashes.Count, Is.EqualTo(1));
|
||||||
|
|
||||||
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
|
Assert.That(collections[1].Name, Is.EqualTo("Second"));
|
||||||
Assert.That(osu.CollectionManager.Collections[1].BeatmapHashes.Count, Is.EqualTo(12));
|
Assert.That(collections[1].BeatmapMD5Hashes.Count, Is.EqualTo(12));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -80,13 +91,18 @@ namespace osu.Game.Tests.Collections.IO
|
|||||||
|
|
||||||
await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
|
await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
|
||||||
|
|
||||||
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
|
osu.Realm.Run(realm =>
|
||||||
|
{
|
||||||
|
var collections = realm.All<BeatmapCollection>().ToList();
|
||||||
|
|
||||||
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
|
Assert.That(collections.Count, Is.EqualTo(2));
|
||||||
Assert.That(osu.CollectionManager.Collections[0].BeatmapHashes.Count, Is.EqualTo(1));
|
|
||||||
|
|
||||||
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
|
Assert.That(collections[0].Name, Is.EqualTo("First"));
|
||||||
Assert.That(osu.CollectionManager.Collections[1].BeatmapHashes.Count, Is.EqualTo(12));
|
Assert.That(collections[0].BeatmapMD5Hashes.Count, Is.EqualTo(1));
|
||||||
|
|
||||||
|
Assert.That(collections[1].Name, Is.EqualTo("Second"));
|
||||||
|
Assert.That(collections[1].BeatmapMD5Hashes.Count, Is.EqualTo(12));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -123,7 +139,11 @@ namespace osu.Game.Tests.Collections.IO
|
|||||||
}
|
}
|
||||||
|
|
||||||
Assert.That(exceptionThrown, Is.False);
|
Assert.That(exceptionThrown, Is.False);
|
||||||
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(0));
|
osu.Realm.Run(realm =>
|
||||||
|
{
|
||||||
|
var collections = realm.All<BeatmapCollection>().ToList();
|
||||||
|
Assert.That(collections.Count, Is.EqualTo(0));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -148,12 +168,18 @@ namespace osu.Game.Tests.Collections.IO
|
|||||||
|
|
||||||
await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
|
await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
|
||||||
|
|
||||||
// Move first beatmap from second collection into the first.
|
// ReSharper disable once MethodHasAsyncOverload
|
||||||
osu.CollectionManager.Collections[0].BeatmapHashes.Add(osu.CollectionManager.Collections[1].BeatmapHashes[0]);
|
osu.Realm.Write(realm =>
|
||||||
osu.CollectionManager.Collections[1].BeatmapHashes.RemoveAt(0);
|
{
|
||||||
|
var collections = realm.All<BeatmapCollection>().ToList();
|
||||||
|
|
||||||
// Rename the second collecction.
|
// Move first beatmap from second collection into the first.
|
||||||
osu.CollectionManager.Collections[1].Name.Value = "Another";
|
collections[0].BeatmapMD5Hashes.Add(collections[1].BeatmapMD5Hashes[0]);
|
||||||
|
collections[1].BeatmapMD5Hashes.RemoveAt(0);
|
||||||
|
|
||||||
|
// Rename the second collecction.
|
||||||
|
collections[1].Name = "Another";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -168,13 +194,17 @@ namespace osu.Game.Tests.Collections.IO
|
|||||||
{
|
{
|
||||||
var osu = LoadOsuIntoHost(host, true);
|
var osu = LoadOsuIntoHost(host, true);
|
||||||
|
|
||||||
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
|
osu.Realm.Run(realm =>
|
||||||
|
{
|
||||||
|
var collections = realm.All<BeatmapCollection>().ToList();
|
||||||
|
Assert.That(collections.Count, Is.EqualTo(2));
|
||||||
|
|
||||||
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
|
Assert.That(collections[0].Name, Is.EqualTo("First"));
|
||||||
Assert.That(osu.CollectionManager.Collections[0].BeatmapHashes.Count, Is.EqualTo(2));
|
Assert.That(collections[0].BeatmapMD5Hashes.Count, Is.EqualTo(2));
|
||||||
|
|
||||||
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Another"));
|
Assert.That(collections[1].Name, Is.EqualTo("Another"));
|
||||||
Assert.That(osu.CollectionManager.Collections[1].BeatmapHashes.Count, Is.EqualTo(11));
|
Assert.That(collections[1].BeatmapMD5Hashes.Count, Is.EqualTo(11));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -187,7 +217,7 @@ namespace osu.Game.Tests.Collections.IO
|
|||||||
{
|
{
|
||||||
// intentionally spin this up on a separate task to avoid disposal deadlocks.
|
// intentionally spin this up on a separate task to avoid disposal deadlocks.
|
||||||
// see https://github.com/EventStore/EventStore/issues/1179
|
// see https://github.com/EventStore/EventStore/issues/1179
|
||||||
await Task.Factory.StartNew(() => osu.CollectionManager.Import(stream).WaitSafely(), TaskCreationOptions.LongRunning);
|
await Task.Factory.StartNew(() => new LegacyCollectionImporter(osu.Realm).Import(stream).WaitSafely(), TaskCreationOptions.LongRunning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game.Collections;
|
using osu.Game.Database;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
namespace osu.Game.Tests
|
namespace osu.Game.Tests
|
||||||
@ -47,7 +47,7 @@ namespace osu.Game.Tests
|
|||||||
|
|
||||||
public class TestOsuGameBase : OsuGameBase
|
public class TestOsuGameBase : OsuGameBase
|
||||||
{
|
{
|
||||||
public CollectionManager CollectionManager { get; private set; }
|
public RealmAccess Realm => Dependencies.Get<RealmAccess>();
|
||||||
|
|
||||||
private readonly bool withBeatmap;
|
private readonly bool withBeatmap;
|
||||||
|
|
||||||
@ -62,8 +62,6 @@ namespace osu.Game.Tests
|
|||||||
// Beatmap must be imported before the collection manager is loaded.
|
// Beatmap must be imported before the collection manager is loaded.
|
||||||
if (withBeatmap)
|
if (withBeatmap)
|
||||||
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely();
|
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely();
|
||||||
|
|
||||||
AddInternal(CollectionManager = new CollectionManager(Storage));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -27,12 +25,9 @@ namespace osu.Game.Tests.Visual.Collections
|
|||||||
{
|
{
|
||||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||||
|
|
||||||
private DialogOverlay dialogOverlay;
|
private DialogOverlay dialogOverlay = null!;
|
||||||
private CollectionManager manager;
|
private BeatmapManager beatmapManager = null!;
|
||||||
|
private ManageCollectionsDialog dialog = null!;
|
||||||
private BeatmapManager beatmapManager;
|
|
||||||
|
|
||||||
private ManageCollectionsDialog dialog;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host)
|
private void load(GameHost host)
|
||||||
@ -45,19 +40,17 @@ namespace osu.Game.Tests.Visual.Collections
|
|||||||
|
|
||||||
base.Content.AddRange(new Drawable[]
|
base.Content.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
manager = new CollectionManager(LocalStorage),
|
|
||||||
Content,
|
Content,
|
||||||
dialogOverlay = new DialogOverlay(),
|
dialogOverlay = new DialogOverlay(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Dependencies.Cache(manager);
|
|
||||||
Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);
|
Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUp() => Schedule(() =>
|
||||||
{
|
{
|
||||||
manager.Collections.Clear();
|
Realm.Write(r => r.RemoveAll<BeatmapCollection>());
|
||||||
Child = dialog = new ManageCollectionsDialog();
|
Child = dialog = new ManageCollectionsDialog();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -77,17 +70,17 @@ namespace osu.Game.Tests.Visual.Collections
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestLastItemIsPlaceholder()
|
public void TestLastItemIsPlaceholder()
|
||||||
{
|
{
|
||||||
AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model));
|
AddAssert("last item is placeholder", () => !dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model.IsManaged);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestAddCollectionExternal()
|
public void TestAddCollectionExternal()
|
||||||
{
|
{
|
||||||
AddStep("add collection", () => manager.Collections.Add(new BeatmapCollection { Name = { Value = "First collection" } }));
|
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "First collection"))));
|
||||||
assertCollectionCount(1);
|
assertCollectionCount(1);
|
||||||
assertCollectionName(0, "First collection");
|
assertCollectionName(0, "First collection");
|
||||||
|
|
||||||
AddStep("add another collection", () => manager.Collections.Add(new BeatmapCollection { Name = { Value = "Second collection" } }));
|
AddStep("add another collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "Second collection"))));
|
||||||
assertCollectionCount(2);
|
assertCollectionCount(2);
|
||||||
assertCollectionName(1, "Second collection");
|
assertCollectionName(1, "Second collection");
|
||||||
}
|
}
|
||||||
@ -107,7 +100,7 @@ namespace osu.Game.Tests.Visual.Collections
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestAddCollectionViaPlaceholder()
|
public void TestAddCollectionViaPlaceholder()
|
||||||
{
|
{
|
||||||
DrawableCollectionListItem placeholderItem = null;
|
DrawableCollectionListItem placeholderItem = null!;
|
||||||
|
|
||||||
AddStep("focus placeholder", () =>
|
AddStep("focus placeholder", () =>
|
||||||
{
|
{
|
||||||
@ -115,24 +108,37 @@ namespace osu.Game.Tests.Visual.Collections
|
|||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Done directly via the collection since InputManager methods cannot add text to textbox...
|
assertCollectionCount(0);
|
||||||
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));
|
AddStep("change collection name", () =>
|
||||||
|
{
|
||||||
|
placeholderItem.ChildrenOfType<TextBox>().First().Text = "test text";
|
||||||
|
InputManager.Key(Key.Enter);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertCollectionCount(1);
|
||||||
|
|
||||||
|
AddAssert("last item is placeholder", () => !dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model.IsManaged);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRemoveCollectionExternal()
|
public void TestRemoveCollectionExternal()
|
||||||
{
|
{
|
||||||
AddStep("add two collections", () => manager.Collections.AddRange(new[]
|
BeatmapCollection first = null!;
|
||||||
{
|
|
||||||
new BeatmapCollection { Name = { Value = "1" } },
|
|
||||||
new BeatmapCollection { Name = { Value = "2" } },
|
|
||||||
}));
|
|
||||||
|
|
||||||
AddStep("remove first collection", () => manager.Collections.RemoveAt(0));
|
AddStep("add two collections", () =>
|
||||||
|
{
|
||||||
|
Realm.Write(r =>
|
||||||
|
{
|
||||||
|
r.Add(new[]
|
||||||
|
{
|
||||||
|
first = new BeatmapCollection(name: "1"),
|
||||||
|
new BeatmapCollection(name: "2"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("remove first collection", () => Realm.Write(r => r.Remove(first)));
|
||||||
assertCollectionCount(1);
|
assertCollectionCount(1);
|
||||||
assertCollectionName(0, "2");
|
assertCollectionName(0, "2");
|
||||||
}
|
}
|
||||||
@ -142,7 +148,7 @@ namespace osu.Game.Tests.Visual.Collections
|
|||||||
{
|
{
|
||||||
AddStep("add dropdown", () =>
|
AddStep("add dropdown", () =>
|
||||||
{
|
{
|
||||||
Add(new CollectionFilterDropdown
|
Add(new CollectionDropdown
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopRight,
|
Anchor = Anchor.TopRight,
|
||||||
Origin = Anchor.TopRight,
|
Origin = Anchor.TopRight,
|
||||||
@ -150,21 +156,27 @@ namespace osu.Game.Tests.Visual.Collections
|
|||||||
Width = 0.4f,
|
Width = 0.4f,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
AddStep("add two collections with same name", () => manager.Collections.AddRange(new[]
|
AddStep("add two collections with same name", () => Realm.Write(r => r.Add(new[]
|
||||||
{
|
{
|
||||||
new BeatmapCollection { Name = { Value = "1" } },
|
new BeatmapCollection(name: "1"),
|
||||||
new BeatmapCollection { Name = { Value = "1" }, BeatmapHashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } },
|
new BeatmapCollection(name: "1")
|
||||||
}));
|
{
|
||||||
|
BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash }
|
||||||
|
},
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRemoveCollectionViaButton()
|
public void TestRemoveCollectionViaButton()
|
||||||
{
|
{
|
||||||
AddStep("add two collections", () => manager.Collections.AddRange(new[]
|
AddStep("add two collections", () => Realm.Write(r => r.Add(new[]
|
||||||
{
|
{
|
||||||
new BeatmapCollection { Name = { Value = "1" } },
|
new BeatmapCollection(name: "1"),
|
||||||
new BeatmapCollection { Name = { Value = "2" }, BeatmapHashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } },
|
new BeatmapCollection(name: "2")
|
||||||
}));
|
{
|
||||||
|
BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash }
|
||||||
|
},
|
||||||
|
})));
|
||||||
|
|
||||||
assertCollectionCount(2);
|
assertCollectionCount(2);
|
||||||
|
|
||||||
@ -197,10 +209,13 @@ namespace osu.Game.Tests.Visual.Collections
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCollectionNotRemovedWhenDialogCancelled()
|
public void TestCollectionNotRemovedWhenDialogCancelled()
|
||||||
{
|
{
|
||||||
AddStep("add two collections", () => manager.Collections.AddRange(new[]
|
AddStep("add collection", () => Realm.Write(r => r.Add(new[]
|
||||||
{
|
{
|
||||||
new BeatmapCollection { Name = { Value = "1" }, BeatmapHashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } },
|
new BeatmapCollection(name: "1")
|
||||||
}));
|
{
|
||||||
|
BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash }
|
||||||
|
},
|
||||||
|
})));
|
||||||
|
|
||||||
assertCollectionCount(1);
|
assertCollectionCount(1);
|
||||||
|
|
||||||
@ -223,34 +238,67 @@ namespace osu.Game.Tests.Visual.Collections
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCollectionRenamedExternal()
|
public void TestCollectionRenamedExternal()
|
||||||
{
|
{
|
||||||
AddStep("add two collections", () => manager.Collections.AddRange(new[]
|
BeatmapCollection first = null!;
|
||||||
|
|
||||||
|
AddStep("add two collections", () =>
|
||||||
{
|
{
|
||||||
new BeatmapCollection { Name = { Value = "1" } },
|
Realm.Write(r =>
|
||||||
new BeatmapCollection { Name = { Value = "2" } },
|
{
|
||||||
}));
|
r.Add(new[]
|
||||||
|
{
|
||||||
|
first = new BeatmapCollection(name: "1"),
|
||||||
|
new BeatmapCollection(name: "2"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
AddStep("change first collection name", () => manager.Collections[0].Name.Value = "First");
|
assertCollectionName(0, "1");
|
||||||
|
assertCollectionName(1, "2");
|
||||||
|
|
||||||
assertCollectionName(0, "First");
|
AddStep("change first collection name", () => Realm.Write(_ => first.Name = "First"));
|
||||||
|
|
||||||
|
// Item will have moved due to alphabetical sorting.
|
||||||
|
assertCollectionName(0, "2");
|
||||||
|
assertCollectionName(1, "First");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCollectionRenamedOnTextChange()
|
public void TestCollectionRenamedOnTextChange()
|
||||||
{
|
{
|
||||||
AddStep("add two collections", () => manager.Collections.AddRange(new[]
|
BeatmapCollection first = null!;
|
||||||
|
DrawableCollectionListItem firstItem = null!;
|
||||||
|
|
||||||
|
AddStep("add two collections", () =>
|
||||||
{
|
{
|
||||||
new BeatmapCollection { Name = { Value = "1" } },
|
Realm.Write(r =>
|
||||||
new BeatmapCollection { Name = { Value = "2" } },
|
{
|
||||||
}));
|
r.Add(new[]
|
||||||
|
{
|
||||||
|
first = new BeatmapCollection(name: "1"),
|
||||||
|
new BeatmapCollection(name: "2"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
assertCollectionCount(2);
|
assertCollectionCount(2);
|
||||||
|
|
||||||
AddStep("change first collection name", () => dialog.ChildrenOfType<TextBox>().First().Text = "First");
|
AddStep("focus first collection", () =>
|
||||||
AddAssert("collection has new name", () => manager.Collections[0].Name.Value == "First");
|
{
|
||||||
|
InputManager.MoveMouseTo(firstItem = dialog.ChildrenOfType<DrawableCollectionListItem>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("change first collection name", () =>
|
||||||
|
{
|
||||||
|
firstItem.ChildrenOfType<TextBox>().First().Text = "First";
|
||||||
|
InputManager.Key(Key.Enter);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("collection has new name", () => first.Name == "First");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertCollectionCount(int count)
|
private void assertCollectionCount(int count)
|
||||||
=> AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType<DrawableCollectionListItem>().Count(i => i.IsCreated.Value) == count);
|
=> AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType<DrawableCollectionListItem>().Count() == count + 1); // +1 for placeholder
|
||||||
|
|
||||||
private void assertCollectionName(int index, string name)
|
private void assertCollectionName(int index, string name)
|
||||||
=> AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType<DrawableCollectionListItem>().ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);
|
=> AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType<DrawableCollectionListItem>().ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);
|
||||||
|
@ -74,14 +74,14 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddStep("set filter again", () => songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value = "test");
|
AddStep("set filter again", () => songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value = "test");
|
||||||
AddStep("open collections dropdown", () =>
|
AddStep("open collections dropdown", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(songSelect.ChildrenOfType<CollectionFilterDropdown>().Single());
|
InputManager.MoveMouseTo(songSelect.ChildrenOfType<CollectionDropdown>().Single());
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("press back once", () => InputManager.Click(MouseButton.Button1));
|
AddStep("press back once", () => InputManager.Click(MouseButton.Button1));
|
||||||
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
|
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
|
||||||
AddAssert("collections dropdown closed", () => songSelect
|
AddAssert("collections dropdown closed", () => songSelect
|
||||||
.ChildrenOfType<CollectionFilterDropdown>().Single()
|
.ChildrenOfType<CollectionDropdown>().Single()
|
||||||
.ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu>().Single().State == MenuState.Closed);
|
.ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu>().Single().State == MenuState.Closed);
|
||||||
|
|
||||||
AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1));
|
AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1));
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -28,11 +26,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||||
|
|
||||||
private CollectionManager collectionManager;
|
private BeatmapManager beatmapManager = null!;
|
||||||
|
private FilterControl control = null!;
|
||||||
private BeatmapManager beatmapManager;
|
|
||||||
|
|
||||||
private FilterControl control;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host)
|
private void load(GameHost host)
|
||||||
@ -45,17 +40,14 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
base.Content.AddRange(new Drawable[]
|
base.Content.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
collectionManager = new CollectionManager(LocalStorage),
|
|
||||||
Content
|
Content
|
||||||
});
|
});
|
||||||
|
|
||||||
Dependencies.Cache(collectionManager);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUp() => Schedule(() =>
|
||||||
{
|
{
|
||||||
collectionManager.Collections.Clear();
|
Realm.Write(r => r.RemoveAll<BeatmapCollection>());
|
||||||
|
|
||||||
Child = control = new FilterControl
|
Child = control = new FilterControl
|
||||||
{
|
{
|
||||||
@ -76,8 +68,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCollectionAddedToDropdown()
|
public void TestCollectionAddedToDropdown()
|
||||||
{
|
{
|
||||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } }));
|
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||||
assertCollectionDropdownContains("1");
|
assertCollectionDropdownContains("1");
|
||||||
assertCollectionDropdownContains("2");
|
assertCollectionDropdownContains("2");
|
||||||
}
|
}
|
||||||
@ -85,9 +77,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCollectionRemovedFromDropdown()
|
public void TestCollectionRemovedFromDropdown()
|
||||||
{
|
{
|
||||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
BeatmapCollection first = null!;
|
||||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } }));
|
|
||||||
AddStep("remove collection", () => collectionManager.Collections.RemoveAt(0));
|
AddStep("add collection", () => Realm.Write(r => r.Add(first = new BeatmapCollection(name: "1"))));
|
||||||
|
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||||
|
AddStep("remove collection", () => Realm.Write(r => r.Remove(first)));
|
||||||
|
|
||||||
assertCollectionDropdownContains("1", false);
|
assertCollectionDropdownContains("1", false);
|
||||||
assertCollectionDropdownContains("2");
|
assertCollectionDropdownContains("2");
|
||||||
@ -96,16 +90,16 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCollectionRenamed()
|
public void TestCollectionRenamed()
|
||||||
{
|
{
|
||||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||||
AddStep("select collection", () =>
|
AddStep("select collection", () =>
|
||||||
{
|
{
|
||||||
var dropdown = control.ChildrenOfType<CollectionFilterDropdown>().Single();
|
var dropdown = control.ChildrenOfType<CollectionDropdown>().Single();
|
||||||
dropdown.Current.Value = dropdown.ItemSource.ElementAt(1);
|
dropdown.Current.Value = dropdown.ItemSource.ElementAt(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
addExpandHeaderStep();
|
addExpandHeaderStep();
|
||||||
|
|
||||||
AddStep("change name", () => collectionManager.Collections[0].Name.Value = "First");
|
AddStep("change name", () => Realm.Write(_ => getFirstCollection().Name = "First"));
|
||||||
|
|
||||||
assertCollectionDropdownContains("First");
|
assertCollectionDropdownContains("First");
|
||||||
assertCollectionHeaderDisplays("First");
|
assertCollectionHeaderDisplays("First");
|
||||||
@ -123,7 +117,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
public void TestCollectionFilterHasAddButton()
|
public void TestCollectionFilterHasAddButton()
|
||||||
{
|
{
|
||||||
addExpandHeaderStep();
|
addExpandHeaderStep();
|
||||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||||
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
|
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
|
||||||
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
|
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
|
||||||
}
|
}
|
||||||
@ -133,7 +127,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
addExpandHeaderStep();
|
addExpandHeaderStep();
|
||||||
|
|
||||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||||
|
|
||||||
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||||
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
|
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
|
||||||
@ -149,13 +143,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||||
|
|
||||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||||
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
|
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
|
||||||
|
|
||||||
AddStep("add beatmap to collection", () => collectionManager.Collections[0].BeatmapHashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash));
|
AddStep("add beatmap to collection", () => Realm.Write(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash)));
|
||||||
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
|
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
|
||||||
|
|
||||||
AddStep("remove beatmap from collection", () => collectionManager.Collections[0].BeatmapHashes.Clear());
|
AddStep("remove beatmap from collection", () => Realm.Write(r => getFirstCollection().BeatmapMD5Hashes.Clear()));
|
||||||
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
|
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,24 +160,26 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
|
||||||
|
|
||||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||||
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
|
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
|
||||||
|
|
||||||
addClickAddOrRemoveButtonStep(1);
|
addClickAddOrRemoveButtonStep(1);
|
||||||
AddAssert("collection contains beatmap", () => collectionManager.Collections[0].BeatmapHashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||||
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
|
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
|
||||||
|
|
||||||
addClickAddOrRemoveButtonStep(1);
|
addClickAddOrRemoveButtonStep(1);
|
||||||
AddAssert("collection does not contain beatmap", () => !collectionManager.Collections[0].BeatmapHashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
|
||||||
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
|
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestManageCollectionsFilterIsNotSelected()
|
public void TestManageCollectionsFilterIsNotSelected()
|
||||||
{
|
{
|
||||||
|
bool received = false;
|
||||||
|
|
||||||
addExpandHeaderStep();
|
addExpandHeaderStep();
|
||||||
|
|
||||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1", new List<string> { "abc" }))));
|
||||||
AddStep("select collection", () =>
|
AddStep("select collection", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1));
|
InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1));
|
||||||
@ -192,18 +188,28 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
addExpandHeaderStep();
|
addExpandHeaderStep();
|
||||||
|
|
||||||
|
AddStep("watch for filter requests", () =>
|
||||||
|
{
|
||||||
|
received = false;
|
||||||
|
control.ChildrenOfType<CollectionDropdown>().First().RequestFilter = () => received = true;
|
||||||
|
});
|
||||||
|
|
||||||
AddStep("click manage collections filter", () =>
|
AddStep("click manage collections filter", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(getCollectionDropdownItems().Last());
|
InputManager.MoveMouseTo(getCollectionDropdownItems().Last());
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("collection filter still selected", () => control.CreateCriteria().Collection?.Name.Value == "1");
|
AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes.Any());
|
||||||
|
|
||||||
|
AddAssert("filter request not fired", () => !received);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All<BeatmapCollection>().First());
|
||||||
|
|
||||||
private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true)
|
private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true)
|
||||||
=> AddAssert($"collection dropdown header displays '{collectionName}'",
|
=> AddAssert($"collection dropdown header displays '{collectionName}'",
|
||||||
() => shouldDisplay == (control.ChildrenOfType<CollectionFilterDropdown.CollectionDropdownHeader>().Single().ChildrenOfType<SpriteText>().First().Text == collectionName));
|
() => shouldDisplay == (control.ChildrenOfType<CollectionDropdown.CollectionDropdownHeader>().Single().ChildrenOfType<SpriteText>().First().Text == collectionName));
|
||||||
|
|
||||||
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
|
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
|
||||||
AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
|
AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
|
||||||
@ -215,7 +221,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
private void addExpandHeaderStep() => AddStep("expand header", () =>
|
private void addExpandHeaderStep() => AddStep("expand header", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(control.ChildrenOfType<CollectionFilterDropdown.CollectionDropdownHeader>().Single());
|
InputManager.MoveMouseTo(control.ChildrenOfType<CollectionDropdown.CollectionDropdownHeader>().Single());
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -226,6 +232,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
});
|
});
|
||||||
|
|
||||||
private IEnumerable<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems()
|
private IEnumerable<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems()
|
||||||
=> control.ChildrenOfType<CollectionFilterDropdown>().Single().ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem>();
|
=> control.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,49 +1,57 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Bindables;
|
using System.Collections.Generic;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Collections
|
namespace osu.Game.Collections
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A collection of beatmaps grouped by a name.
|
/// A collection of beatmaps grouped by a name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BeatmapCollection
|
public class BeatmapCollection : RealmObject, IHasGuidPrimaryKey
|
||||||
{
|
{
|
||||||
/// <summary>
|
[PrimaryKey]
|
||||||
/// Invoked whenever any change occurs on this <see cref="BeatmapCollection"/>.
|
public Guid ID { get; set; }
|
||||||
/// </summary>
|
|
||||||
public event Action Changed;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The collection's name.
|
/// The collection's name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly Bindable<string> Name = new Bindable<string>();
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="BeatmapInfo.MD5Hash"/>es of beatmaps contained by the collection.
|
/// The <see cref="BeatmapInfo.MD5Hash"/>es of beatmaps contained by the collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly BindableList<string> BeatmapHashes = new BindableList<string>();
|
/// <remarks>
|
||||||
|
/// We store as hashes rather than references to <see cref="BeatmapInfo"/>s to allow collections to maintain
|
||||||
|
/// references to beatmaps even if they are removed. This helps with cases like importing collections before
|
||||||
|
/// importing the beatmaps they contain, or when sharing collections between users.
|
||||||
|
///
|
||||||
|
/// This can probably change in the future as we build the system up.
|
||||||
|
/// </remarks>
|
||||||
|
public IList<string> BeatmapMD5Hashes { get; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The date when this collection was last modified.
|
/// The date when this collection was last modified.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset LastModifyDate { get; private set; } = DateTimeOffset.UtcNow;
|
public DateTimeOffset LastModified { get; set; }
|
||||||
|
|
||||||
public BeatmapCollection()
|
public BeatmapCollection(string? name = null, IList<string>? beatmapMD5Hashes = null)
|
||||||
{
|
{
|
||||||
BeatmapHashes.CollectionChanged += (_, _) => onChange();
|
ID = Guid.NewGuid();
|
||||||
Name.ValueChanged += _ => onChange();
|
Name = name ?? string.Empty;
|
||||||
|
BeatmapMD5Hashes = beatmapMD5Hashes ?? new List<string>();
|
||||||
|
|
||||||
|
LastModified = DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onChange()
|
[UsedImplicitly]
|
||||||
|
private BeatmapCollection()
|
||||||
{
|
{
|
||||||
LastModifyDate = DateTimeOffset.Now;
|
|
||||||
Changed?.Invoke();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
250
osu.Game/Collections/CollectionDropdown.cs
Normal file
250
osu.Game/Collections/CollectionDropdown.cs
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osuTK;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
namespace osu.Game.Collections
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A dropdown to select the collection to be used to filter results.
|
||||||
|
/// </summary>
|
||||||
|
public class CollectionDropdown : OsuDropdown<CollectionFilterMenuItem>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to show the "manage collections..." menu item in the dropdown.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual bool ShowManageCollectionsItem => true;
|
||||||
|
|
||||||
|
public Action? RequestFilter { private get; set; }
|
||||||
|
|
||||||
|
private readonly BindableList<CollectionFilterMenuItem> filters = new BindableList<CollectionFilterMenuItem>();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private RealmAccess realm { get; set; } = null!;
|
||||||
|
|
||||||
|
private IDisposable? realmSubscription;
|
||||||
|
|
||||||
|
public CollectionDropdown()
|
||||||
|
{
|
||||||
|
ItemSource = filters;
|
||||||
|
|
||||||
|
Current.Value = new AllBeatmapsCollectionFilterMenuItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapCollection>().OrderBy(c => c.Name), collectionsChanged);
|
||||||
|
|
||||||
|
Current.BindValueChanged(selectionChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes, Exception error)
|
||||||
|
{
|
||||||
|
var selectedItem = SelectedItem?.Value?.Collection;
|
||||||
|
|
||||||
|
var allBeatmaps = new AllBeatmapsCollectionFilterMenuItem();
|
||||||
|
|
||||||
|
filters.Clear();
|
||||||
|
filters.Add(allBeatmaps);
|
||||||
|
filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm))));
|
||||||
|
|
||||||
|
if (ShowManageCollectionsItem)
|
||||||
|
filters.Add(new ManageCollectionsFilterMenuItem());
|
||||||
|
|
||||||
|
// This current update and schedule is required to work around dropdown headers not updating text even when the selected item
|
||||||
|
// changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue
|
||||||
|
// a warning that it's going to be a frustrating journey.
|
||||||
|
Current.Value = allBeatmaps;
|
||||||
|
Schedule(() => Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection.ID == selectedItem?.ID) ?? filters[0]);
|
||||||
|
|
||||||
|
// Trigger a re-filter if the current item was in the change set.
|
||||||
|
if (selectedItem != null && changes != null)
|
||||||
|
{
|
||||||
|
foreach (int index in changes.ModifiedIndices)
|
||||||
|
{
|
||||||
|
if (collections[index].ID == selectedItem.ID)
|
||||||
|
RequestFilter?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Live<BeatmapCollection>? lastFiltered;
|
||||||
|
|
||||||
|
private void selectionChanged(ValueChangedEvent<CollectionFilterMenuItem> filter)
|
||||||
|
{
|
||||||
|
// May be null during .Clear().
|
||||||
|
if (filter.NewValue == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 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 ManageCollectionsFilterMenuItem)
|
||||||
|
{
|
||||||
|
Current.Value = filter.OldValue;
|
||||||
|
manageCollectionsDialog?.Show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newCollection = filter.NewValue?.Collection;
|
||||||
|
|
||||||
|
// This dropdown be weird.
|
||||||
|
// We only care about filtering if the actual collection has changed.
|
||||||
|
if (newCollection != lastFiltered)
|
||||||
|
{
|
||||||
|
RequestFilter?.Invoke();
|
||||||
|
lastFiltered = newCollection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
realmSubscription?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName;
|
||||||
|
|
||||||
|
protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader();
|
||||||
|
|
||||||
|
protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu();
|
||||||
|
|
||||||
|
protected virtual CollectionDropdownHeader CreateCollectionHeader() => new CollectionDropdownHeader();
|
||||||
|
|
||||||
|
protected virtual CollectionDropdownMenu CreateCollectionMenu() => new CollectionDropdownMenu();
|
||||||
|
|
||||||
|
public class CollectionDropdownHeader : OsuDropdownHeader
|
||||||
|
{
|
||||||
|
public CollectionDropdownHeader()
|
||||||
|
{
|
||||||
|
Height = 25;
|
||||||
|
Icon.Size = new Vector2(16);
|
||||||
|
Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class CollectionDropdownMenu : OsuDropdownMenu
|
||||||
|
{
|
||||||
|
public CollectionDropdownMenu()
|
||||||
|
{
|
||||||
|
MaxHeight = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownDrawableMenuItem(item)
|
||||||
|
{
|
||||||
|
BackgroundColourHover = HoverColour,
|
||||||
|
BackgroundColourSelected = SelectionColour
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class CollectionDropdownDrawableMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem
|
||||||
|
{
|
||||||
|
private IconButton addOrRemoveButton = null!;
|
||||||
|
|
||||||
|
private bool beatmapInCollection;
|
||||||
|
|
||||||
|
private readonly Live<BeatmapCollection>? collection;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||||
|
|
||||||
|
public CollectionDropdownDrawableMenuItem(MenuItem item)
|
||||||
|
: base(item)
|
||||||
|
{
|
||||||
|
collection = ((DropdownMenuItem<CollectionFilterMenuItem>)item).Value.Collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 (collection != null)
|
||||||
|
{
|
||||||
|
beatmap.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
beatmapInCollection = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.Value.BeatmapInfo.MD5Hash));
|
||||||
|
|
||||||
|
addOrRemoveButton.Enabled.Value = !beatmap.IsDefault;
|
||||||
|
addOrRemoveButton.Icon = beatmapInCollection ? FontAwesome.Solid.MinusSquare : FontAwesome.Solid.PlusSquare;
|
||||||
|
addOrRemoveButton.TooltipText = beatmapInCollection ? "Remove selected beatmap" : "Add selected beatmap";
|
||||||
|
|
||||||
|
updateButtonVisibility();
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtonVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
{
|
||||||
|
updateButtonVisibility();
|
||||||
|
return base.OnHover(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
|
{
|
||||||
|
updateButtonVisibility();
|
||||||
|
base.OnHoverLost(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSelectChange()
|
||||||
|
{
|
||||||
|
base.OnSelectChange();
|
||||||
|
updateButtonVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateButtonVisibility()
|
||||||
|
{
|
||||||
|
if (collection == null)
|
||||||
|
addOrRemoveButton.Alpha = 0;
|
||||||
|
else
|
||||||
|
addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addOrRemove()
|
||||||
|
{
|
||||||
|
Debug.Assert(collection != null);
|
||||||
|
|
||||||
|
collection.PerformWrite(c =>
|
||||||
|
{
|
||||||
|
if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
|
||||||
|
c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Drawable CreateContent() => (Content)base.CreateContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,297 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.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.Framework.Localisation;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Collections
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A dropdown to select the <see cref="CollectionFilterMenuItem"/> to filter beatmaps using.
|
|
||||||
/// </summary>
|
|
||||||
public class CollectionFilterDropdown : OsuDropdown<CollectionFilterMenuItem>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to show the "manage collections..." menu item in the dropdown.
|
|
||||||
/// </summary>
|
|
||||||
protected virtual bool ShowManageCollectionsItem => true;
|
|
||||||
|
|
||||||
private readonly BindableWithCurrent<CollectionFilterMenuItem> current = new BindableWithCurrent<CollectionFilterMenuItem>();
|
|
||||||
|
|
||||||
public new Bindable<CollectionFilterMenuItem> Current
|
|
||||||
{
|
|
||||||
get => current.Current;
|
|
||||||
set => current.Current = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly IBindableList<BeatmapCollection> collections = new BindableList<BeatmapCollection>();
|
|
||||||
private readonly IBindableList<string> beatmaps = new BindableList<string>();
|
|
||||||
private readonly BindableList<CollectionFilterMenuItem> filters = new BindableList<CollectionFilterMenuItem>();
|
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
|
||||||
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
|
||||||
private CollectionManager collectionManager { get; set; }
|
|
||||||
|
|
||||||
public CollectionFilterDropdown()
|
|
||||||
{
|
|
||||||
ItemSource = filters;
|
|
||||||
Current.Value = new AllBeatmapsCollectionFilterMenuItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
if (collectionManager != null)
|
|
||||||
collections.BindTo(collectionManager.Collections);
|
|
||||||
|
|
||||||
// Dropdown has logic which triggers a change on the bindable with every change to the contained items.
|
|
||||||
// This is not desirable here, as it leads to multiple filter operations running even though nothing has changed.
|
|
||||||
// An extra bindable is enough to subvert this behaviour.
|
|
||||||
base.Current = Current;
|
|
||||||
|
|
||||||
collections.BindCollectionChanged((_, _) => collectionsChanged(), true);
|
|
||||||
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 AllBeatmapsCollectionFilterMenuItem());
|
|
||||||
filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c)));
|
|
||||||
|
|
||||||
if (ShowManageCollectionsItem)
|
|
||||||
filters.Add(new ManageCollectionsFilterMenuItem());
|
|
||||||
|
|
||||||
Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection == selectedItem) ?? filters[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Occurs when the <see cref="CollectionFilterMenuItem"/> selection has changed.
|
|
||||||
/// </summary>
|
|
||||||
private void filterChanged(ValueChangedEvent<CollectionFilterMenuItem> 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.BeatmapHashes);
|
|
||||||
|
|
||||||
if (filter.NewValue?.Collection != null)
|
|
||||||
beatmaps.BindTo(filter.NewValue.Collection.BeatmapHashes);
|
|
||||||
|
|
||||||
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 ManageCollectionsFilterMenuItem)
|
|
||||||
{
|
|
||||||
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 LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName.Value;
|
|
||||||
|
|
||||||
protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader().With(d =>
|
|
||||||
{
|
|
||||||
d.SelectedItem.BindTarget = Current;
|
|
||||||
});
|
|
||||||
|
|
||||||
protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu();
|
|
||||||
|
|
||||||
protected virtual CollectionDropdownHeader CreateCollectionHeader() => new CollectionDropdownHeader();
|
|
||||||
|
|
||||||
protected virtual CollectionDropdownMenu CreateCollectionMenu() => new CollectionDropdownMenu();
|
|
||||||
|
|
||||||
public class CollectionDropdownHeader : OsuDropdownHeader
|
|
||||||
{
|
|
||||||
public readonly Bindable<CollectionFilterMenuItem> SelectedItem = new Bindable<CollectionFilterMenuItem>();
|
|
||||||
private readonly Bindable<string> collectionName = new Bindable<string>();
|
|
||||||
|
|
||||||
protected override LocalisableString 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected class CollectionDropdownMenu : OsuDropdownMenu
|
|
||||||
{
|
|
||||||
public CollectionDropdownMenu()
|
|
||||||
{
|
|
||||||
MaxHeight = 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item)
|
|
||||||
{
|
|
||||||
BackgroundColourHover = HoverColour,
|
|
||||||
BackgroundColourSelected = SelectionColour
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem
|
|
||||||
{
|
|
||||||
[NotNull]
|
|
||||||
protected new CollectionFilterMenuItem Item => ((DropdownMenuItem<CollectionFilterMenuItem>)base.Item).Value;
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private IBindable<WorkingBeatmap> beatmap { get; set; }
|
|
||||||
|
|
||||||
[CanBeNull]
|
|
||||||
private readonly BindableList<string> 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?.BeatmapHashes.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.MD5Hash);
|
|
||||||
|
|
||||||
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.MD5Hash))
|
|
||||||
collectionBeatmaps.Add(beatmap.Value.BeatmapInfo.MD5Hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Drawable CreateContent() => content = (Content)base.CreateContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using JetBrains.Annotations;
|
using osu.Game.Database;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
|
|
||||||
namespace osu.Game.Collections
|
namespace osu.Game.Collections
|
||||||
{
|
{
|
||||||
@ -18,26 +15,29 @@ namespace osu.Game.Collections
|
|||||||
/// The collection to filter beatmaps from.
|
/// The collection to filter beatmaps from.
|
||||||
/// May be null to not filter by collection (include all beatmaps).
|
/// May be null to not filter by collection (include all beatmaps).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[CanBeNull]
|
public readonly Live<BeatmapCollection>? Collection;
|
||||||
public readonly BeatmapCollection Collection;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the collection.
|
/// The name of the collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[NotNull]
|
public string CollectionName { get; }
|
||||||
public readonly Bindable<string> CollectionName;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="CollectionFilterMenuItem"/>.
|
/// Creates a new <see cref="CollectionFilterMenuItem"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The collection to filter beatmaps from.</param>
|
/// <param name="collection">The collection to filter beatmaps from.</param>
|
||||||
public CollectionFilterMenuItem([CanBeNull] BeatmapCollection collection)
|
public CollectionFilterMenuItem(Live<BeatmapCollection> collection)
|
||||||
|
: this(collection.PerformRead(c => c.Name))
|
||||||
{
|
{
|
||||||
Collection = collection;
|
Collection = collection;
|
||||||
CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable<string>("All beatmaps");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(CollectionFilterMenuItem other)
|
protected CollectionFilterMenuItem(string name)
|
||||||
|
{
|
||||||
|
CollectionName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(CollectionFilterMenuItem? other)
|
||||||
{
|
{
|
||||||
if (other == null)
|
if (other == null)
|
||||||
return false;
|
return false;
|
||||||
@ -45,20 +45,20 @@ namespace osu.Game.Collections
|
|||||||
// collections may have the same name, so compare first on reference equality.
|
// collections may have the same name, so compare first on reference equality.
|
||||||
// this relies on the assumption that only one instance of the BeatmapCollection exists game-wide, managed by CollectionManager.
|
// this relies on the assumption that only one instance of the BeatmapCollection exists game-wide, managed by CollectionManager.
|
||||||
if (Collection != null)
|
if (Collection != null)
|
||||||
return Collection == other.Collection;
|
return Collection.ID == other.Collection?.ID;
|
||||||
|
|
||||||
// fallback to name-based comparison.
|
// fallback to name-based comparison.
|
||||||
// this is required for special dropdown items which don't have a collection (all beatmaps / manage collections items below).
|
// this is required for special dropdown items which don't have a collection (all beatmaps / manage collections items below).
|
||||||
return CollectionName.Value == other.CollectionName.Value;
|
return CollectionName == other.CollectionName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int GetHashCode() => CollectionName.Value.GetHashCode();
|
public override int GetHashCode() => CollectionName.GetHashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem
|
public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem
|
||||||
{
|
{
|
||||||
public AllBeatmapsCollectionFilterMenuItem()
|
public AllBeatmapsCollectionFilterMenuItem()
|
||||||
: base(null)
|
: base("All beatmaps")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,9 +66,8 @@ namespace osu.Game.Collections
|
|||||||
public class ManageCollectionsFilterMenuItem : CollectionFilterMenuItem
|
public class ManageCollectionsFilterMenuItem : CollectionFilterMenuItem
|
||||||
{
|
{
|
||||||
public ManageCollectionsFilterMenuItem()
|
public ManageCollectionsFilterMenuItem()
|
||||||
: base(null)
|
: base("Manage collections...")
|
||||||
{
|
{
|
||||||
CollectionName.Value = "Manage collections...";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,349 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Logging;
|
|
||||||
using osu.Framework.Platform;
|
|
||||||
using osu.Game.Database;
|
|
||||||
using osu.Game.IO;
|
|
||||||
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, IPostNotifications
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Database version in stable-compatible YYYYMMDD format.
|
|
||||||
/// </summary>
|
|
||||||
private const int database_version = 30000000;
|
|
||||||
|
|
||||||
private const string database_name = "collection.db";
|
|
||||||
private const string database_backup_name = "collection.db.bak";
|
|
||||||
|
|
||||||
public readonly BindableList<BeatmapCollection> Collections = new BindableList<BeatmapCollection>();
|
|
||||||
|
|
||||||
private readonly Storage storage;
|
|
||||||
|
|
||||||
public CollectionManager(Storage storage)
|
|
||||||
{
|
|
||||||
this.storage = storage;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
|
||||||
private DatabaseContextFactory efContextFactory { get; set; } = null!;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
efContextFactory?.WaitForMigrationCompletion();
|
|
||||||
|
|
||||||
Collections.CollectionChanged += collectionsChanged;
|
|
||||||
|
|
||||||
if (storage.Exists(database_backup_name))
|
|
||||||
{
|
|
||||||
// If a backup file exists, it means the previous write operation didn't run to completion.
|
|
||||||
// Always prefer the backup file in such a case as it's the most recent copy that is guaranteed to not be malformed.
|
|
||||||
//
|
|
||||||
// The database is saved 100ms after any change, and again when the game is closed, so there shouldn't be a large diff between the two files in the worst case.
|
|
||||||
if (storage.Exists(database_name))
|
|
||||||
storage.Delete(database_name);
|
|
||||||
File.Copy(storage.GetFullPath(database_backup_name), storage.GetFullPath(database_name));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storage.Exists(database_name))
|
|
||||||
{
|
|
||||||
List<BeatmapCollection> beatmapCollections;
|
|
||||||
|
|
||||||
using (var stream = storage.GetStream(database_name))
|
|
||||||
beatmapCollections = readCollections(stream);
|
|
||||||
|
|
||||||
// intentionally fire-and-forget async.
|
|
||||||
importCollections(beatmapCollections);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void collectionsChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
public Action<Notification> PostNotification { protected get; set; }
|
|
||||||
|
|
||||||
public Task<int> GetAvailableCount(StableStorage stableStorage)
|
|
||||||
{
|
|
||||||
if (!stableStorage.Exists(database_name))
|
|
||||||
return Task.FromResult(0);
|
|
||||||
|
|
||||||
return Task.Run(() =>
|
|
||||||
{
|
|
||||||
using (var stream = stableStorage.GetStream(database_name))
|
|
||||||
return readCollections(stream).Count;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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(StableStorage stableStorage)
|
|
||||||
{
|
|
||||||
if (!stableStorage.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 = stableStorage.GetStream(database_name))
|
|
||||||
await Import(stream).ConfigureAwait(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Import(Stream stream)
|
|
||||||
{
|
|
||||||
var notification = new ProgressNotification
|
|
||||||
{
|
|
||||||
State = ProgressNotificationState.Active,
|
|
||||||
Text = "Collections import is initialising..."
|
|
||||||
};
|
|
||||||
|
|
||||||
PostNotification?.Invoke(notification);
|
|
||||||
|
|
||||||
var collections = readCollections(stream, notification);
|
|
||||||
await importCollections(collections).ConfigureAwait(false);
|
|
||||||
|
|
||||||
notification.CompletionText = $"Imported {collections.Count} collections";
|
|
||||||
notification.State = ProgressNotificationState.Completed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task importCollections(List<BeatmapCollection> newCollections)
|
|
||||||
{
|
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
|
||||||
|
|
||||||
Schedule(() =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var newCol in newCollections)
|
|
||||||
{
|
|
||||||
var existing = Collections.FirstOrDefault(c => c.Name.Value == newCol.Name.Value);
|
|
||||||
if (existing == null)
|
|
||||||
Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } });
|
|
||||||
|
|
||||||
foreach (string newBeatmap in newCol.BeatmapHashes)
|
|
||||||
{
|
|
||||||
if (!existing.BeatmapHashes.Contains(newBeatmap))
|
|
||||||
existing.BeatmapHashes.Add(newBeatmap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tcs.SetResult(true);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.Error(e, "Failed to import collection.");
|
|
||||||
tcs.SetException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return tcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
collection.BeatmapHashes.Add(checksum);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ProgressCompletionNotification { 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()
|
|
||||||
{
|
|
||||||
int current = Interlocked.Increment(ref lastSave);
|
|
||||||
Task.Delay(100).ContinueWith(_ =>
|
|
||||||
{
|
|
||||||
if (current != lastSave)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!save())
|
|
||||||
backgroundSave();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool save()
|
|
||||||
{
|
|
||||||
lock (saveLock)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref lastSave);
|
|
||||||
|
|
||||||
// This is NOT thread-safe!!
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string tempPath = Path.GetTempFileName();
|
|
||||||
|
|
||||||
using (var ms = new MemoryStream())
|
|
||||||
{
|
|
||||||
using (var sw = new SerializationWriter(ms, true))
|
|
||||||
{
|
|
||||||
sw.Write(database_version);
|
|
||||||
|
|
||||||
var collectionsCopy = Collections.ToArray();
|
|
||||||
sw.Write(collectionsCopy.Length);
|
|
||||||
|
|
||||||
foreach (var c in collectionsCopy)
|
|
||||||
{
|
|
||||||
sw.Write(c.Name.Value);
|
|
||||||
|
|
||||||
string[] beatmapsCopy = c.BeatmapHashes.ToArray();
|
|
||||||
|
|
||||||
sw.Write(beatmapsCopy.Length);
|
|
||||||
|
|
||||||
foreach (string b in beatmapsCopy)
|
|
||||||
sw.Write(b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var fs = File.OpenWrite(tempPath))
|
|
||||||
ms.WriteTo(fs);
|
|
||||||
|
|
||||||
string databasePath = storage.GetFullPath(database_name);
|
|
||||||
string databaseBackupPath = storage.GetFullPath(database_backup_name);
|
|
||||||
|
|
||||||
// Back up the existing database, clearing any existing backup.
|
|
||||||
if (File.Exists(databaseBackupPath))
|
|
||||||
File.Delete(databaseBackupPath);
|
|
||||||
if (File.Exists(databasePath))
|
|
||||||
File.Move(databasePath, databaseBackupPath);
|
|
||||||
|
|
||||||
// Move the new database in-place of the existing one.
|
|
||||||
File.Move(tempPath, databasePath);
|
|
||||||
|
|
||||||
// If everything succeeded up to this point, remove the backup file.
|
|
||||||
if (File.Exists(databaseBackupPath))
|
|
||||||
File.Delete(databaseBackupPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,22 +2,26 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
|
||||||
namespace osu.Game.Collections
|
namespace osu.Game.Collections
|
||||||
{
|
{
|
||||||
public class CollectionToggleMenuItem : ToggleMenuItem
|
public class CollectionToggleMenuItem : ToggleMenuItem
|
||||||
{
|
{
|
||||||
public CollectionToggleMenuItem(BeatmapCollection collection, IBeatmapInfo beatmap)
|
public CollectionToggleMenuItem(Live<BeatmapCollection> collection, IBeatmapInfo beatmap)
|
||||||
: base(collection.Name.Value, MenuItemType.Standard, state =>
|
: base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state =>
|
||||||
{
|
{
|
||||||
if (state)
|
collection.PerformWrite(c =>
|
||||||
collection.BeatmapHashes.Add(beatmap.MD5Hash);
|
{
|
||||||
else
|
if (state)
|
||||||
collection.BeatmapHashes.Remove(beatmap.MD5Hash);
|
c.BeatmapMD5Hashes.Add(beatmap.MD5Hash);
|
||||||
|
else
|
||||||
|
c.BeatmapMD5Hashes.Remove(beatmap.MD5Hash);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
State.Value = collection.BeatmapHashes.Contains(beatmap.MD5Hash);
|
State.Value = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.MD5Hash));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,20 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Overlays.Dialog;
|
using osu.Game.Overlays.Dialog;
|
||||||
|
|
||||||
namespace osu.Game.Collections
|
namespace osu.Game.Collections
|
||||||
{
|
{
|
||||||
public class DeleteCollectionDialog : PopupDialog
|
public class DeleteCollectionDialog : PopupDialog
|
||||||
{
|
{
|
||||||
public DeleteCollectionDialog(BeatmapCollection collection, Action deleteAction)
|
public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction)
|
||||||
{
|
{
|
||||||
HeaderText = "Confirm deletion of";
|
HeaderText = "Confirm deletion of";
|
||||||
BodyText = $"{collection.Name.Value} ({"beatmap".ToQuantity(collection.BeatmapHashes.Count)})";
|
BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})");
|
||||||
|
|
||||||
Icon = FontAwesome.Regular.TrashAlt;
|
Icon = FontAwesome.Regular.TrashAlt;
|
||||||
|
|
||||||
|
@ -1,39 +1,66 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Collections
|
namespace osu.Game.Collections
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Visualises a list of <see cref="BeatmapCollection"/>s.
|
/// Visualises a list of <see cref="BeatmapCollection"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DrawableCollectionList : OsuRearrangeableListContainer<BeatmapCollection>
|
public class DrawableCollectionList : OsuRearrangeableListContainer<Live<BeatmapCollection>>
|
||||||
{
|
{
|
||||||
private Scroll scroll;
|
|
||||||
|
|
||||||
protected override ScrollContainer<Drawable> CreateScrollContainer() => scroll = new Scroll();
|
protected override ScrollContainer<Drawable> CreateScrollContainer() => scroll = new Scroll();
|
||||||
|
|
||||||
protected override FillFlowContainer<RearrangeableListItem<BeatmapCollection>> CreateListFillFlowContainer() => new Flow
|
[Resolved]
|
||||||
|
private RealmAccess realm { get; set; } = null!;
|
||||||
|
|
||||||
|
private Scroll scroll = null!;
|
||||||
|
|
||||||
|
private IDisposable? realmSubscription;
|
||||||
|
|
||||||
|
protected override FillFlowContainer<RearrangeableListItem<Live<BeatmapCollection>>> CreateListFillFlowContainer() => new Flow
|
||||||
{
|
{
|
||||||
DragActive = { BindTarget = DragActive }
|
DragActive = { BindTarget = DragActive }
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override OsuRearrangeableListItem<BeatmapCollection> CreateOsuDrawable(BeatmapCollection item)
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
if (item == scroll.PlaceholderItem.Model)
|
base.LoadComplete();
|
||||||
|
|
||||||
|
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapCollection>().OrderBy(c => c.Name), collectionsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes, Exception error)
|
||||||
|
{
|
||||||
|
Items.Clear();
|
||||||
|
Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm)));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override OsuRearrangeableListItem<Live<BeatmapCollection>> CreateOsuDrawable(Live<BeatmapCollection> item)
|
||||||
|
{
|
||||||
|
if (item.ID == scroll.PlaceholderItem.Model.ID)
|
||||||
return scroll.ReplacePlaceholder();
|
return scroll.ReplacePlaceholder();
|
||||||
|
|
||||||
return new DrawableCollectionListItem(item, true);
|
return new DrawableCollectionListItem(item, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
realmSubscription?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The scroll container for this <see cref="DrawableCollectionList"/>.
|
/// 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.
|
/// Contains the main flow of <see cref="DrawableCollectionListItem"/> and attaches a placeholder item to the end of the list.
|
||||||
@ -46,7 +73,7 @@ namespace osu.Game.Collections
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The currently-displayed placeholder item.
|
/// The currently-displayed placeholder item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DrawableCollectionListItem PlaceholderItem { get; private set; }
|
public DrawableCollectionListItem PlaceholderItem { get; private set; } = null!;
|
||||||
|
|
||||||
protected override Container<Drawable> Content => content;
|
protected override Container<Drawable> Content => content;
|
||||||
private readonly Container content;
|
private readonly Container content;
|
||||||
@ -76,6 +103,7 @@ namespace osu.Game.Collections
|
|||||||
});
|
});
|
||||||
|
|
||||||
ReplacePlaceholder();
|
ReplacePlaceholder();
|
||||||
|
Debug.Assert(PlaceholderItem != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -95,7 +123,7 @@ namespace osu.Game.Collections
|
|||||||
var previous = PlaceholderItem;
|
var previous = PlaceholderItem;
|
||||||
|
|
||||||
placeholderContainer.Clear(false);
|
placeholderContainer.Clear(false);
|
||||||
placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection(), false));
|
placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection().ToLiveUnmanaged(), false));
|
||||||
|
|
||||||
return previous;
|
return previous;
|
||||||
}
|
}
|
||||||
@ -104,7 +132,7 @@ namespace osu.Game.Collections
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The flow of <see cref="DrawableCollectionListItem"/>. Disables layout easing unless a drag is in progress.
|
/// The flow of <see cref="DrawableCollectionListItem"/>. Disables layout easing unless a drag is in progress.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private class Flow : FillFlowContainer<RearrangeableListItem<BeatmapCollection>>
|
private class Flow : FillFlowContainer<RearrangeableListItem<Live<BeatmapCollection>>>
|
||||||
{
|
{
|
||||||
public readonly IBindable<bool> DragActive = new Bindable<bool>();
|
public readonly IBindable<bool> DragActive = new Bindable<bool>();
|
||||||
|
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Database;
|
||||||
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;
|
||||||
@ -24,79 +23,62 @@ namespace osu.Game.Collections
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Visualises a <see cref="BeatmapCollection"/> inside a <see cref="DrawableCollectionList"/>.
|
/// Visualises a <see cref="BeatmapCollection"/> inside a <see cref="DrawableCollectionList"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DrawableCollectionListItem : OsuRearrangeableListItem<BeatmapCollection>
|
public class DrawableCollectionListItem : OsuRearrangeableListItem<Live<BeatmapCollection>>
|
||||||
{
|
{
|
||||||
private const float item_height = 35;
|
private const float item_height = 35;
|
||||||
private const float button_width = item_height * 0.75f;
|
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>
|
/// <summary>
|
||||||
/// Creates a new <see cref="DrawableCollectionListItem"/>.
|
/// Creates a new <see cref="DrawableCollectionListItem"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="item">The <see cref="BeatmapCollection"/>.</param>
|
/// <param name="item">The <see cref="BeatmapCollection"/>.</param>
|
||||||
/// <param name="isCreated">Whether <paramref name="item"/> currently exists inside the <see cref="CollectionManager"/>.</param>
|
/// <param name="isCreated">Whether <paramref name="item"/> currently exists inside realm.</param>
|
||||||
public DrawableCollectionListItem(BeatmapCollection item, bool isCreated)
|
public DrawableCollectionListItem(Live<BeatmapCollection> item, bool isCreated)
|
||||||
: base(item)
|
: base(item)
|
||||||
{
|
{
|
||||||
this.isCreated.Value = isCreated;
|
ShowDragHandle.Value = item.IsManaged;
|
||||||
|
|
||||||
ShowDragHandle.BindTo(this.isCreated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Drawable CreateContent() => new ItemContent(Model)
|
protected override Drawable CreateContent() => new ItemContent(Model);
|
||||||
{
|
|
||||||
IsCreated = { BindTarget = isCreated }
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The main content of the <see cref="DrawableCollectionListItem"/>.
|
/// The main content of the <see cref="DrawableCollectionListItem"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private class ItemContent : CircularContainer
|
private class ItemContent : CircularContainer
|
||||||
{
|
{
|
||||||
public readonly Bindable<bool> IsCreated = new Bindable<bool>();
|
private readonly Live<BeatmapCollection> collection;
|
||||||
|
|
||||||
private readonly IBindable<string> collectionName;
|
private ItemTextBox textBox = null!;
|
||||||
private readonly BeatmapCollection collection;
|
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved]
|
||||||
private CollectionManager collectionManager { get; set; }
|
private RealmAccess realm { get; set; } = null!;
|
||||||
|
|
||||||
private Container textBoxPaddingContainer;
|
public ItemContent(Live<BeatmapCollection> collection)
|
||||||
private ItemTextBox textBox;
|
|
||||||
|
|
||||||
public ItemContent(BeatmapCollection collection)
|
|
||||||
{
|
{
|
||||||
this.collection = collection;
|
this.collection = collection;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
Height = item_height;
|
Height = item_height;
|
||||||
Masking = true;
|
Masking = true;
|
||||||
|
|
||||||
collectionName = collection.Name.GetBoundCopy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new[]
|
||||||
{
|
{
|
||||||
new DeleteButton(collection)
|
collection.IsManaged
|
||||||
{
|
? new DeleteButton(collection)
|
||||||
Anchor = Anchor.CentreRight,
|
{
|
||||||
Origin = Anchor.CentreRight,
|
Anchor = Anchor.CentreRight,
|
||||||
IsCreated = { BindTarget = IsCreated },
|
Origin = Anchor.CentreRight,
|
||||||
IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v)
|
IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v)
|
||||||
},
|
}
|
||||||
textBoxPaddingContainer = new Container
|
: Empty(),
|
||||||
|
new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Padding = new MarginPadding { Right = button_width },
|
Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 },
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
textBox = new ItemTextBox
|
textBox = new ItemTextBox
|
||||||
@ -104,7 +86,7 @@ namespace osu.Game.Collections
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Size = Vector2.One,
|
Size = Vector2.One,
|
||||||
CornerRadius = item_height / 2,
|
CornerRadius = item_height / 2,
|
||||||
PlaceholderText = IsCreated.Value ? string.Empty : "Create a new collection"
|
PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection"
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -116,28 +98,18 @@ namespace osu.Game.Collections
|
|||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
// Bind late, as the collection name may change externally while still loading.
|
// Bind late, as the collection name may change externally while still loading.
|
||||||
textBox.Current = collection.Name;
|
textBox.Current.Value = collection.PerformRead(c => c.IsValid ? c.Name : string.Empty);
|
||||||
|
textBox.OnCommit += onCommit;
|
||||||
collectionName.BindValueChanged(_ => createNewCollection(), true);
|
|
||||||
IsCreated.BindValueChanged(created => textBoxPaddingContainer.Padding = new MarginPadding { Right = created.NewValue ? button_width : 0 }, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createNewCollection()
|
private void onCommit(TextBox sender, bool newText)
|
||||||
{
|
{
|
||||||
if (IsCreated.Value)
|
if (collection.IsManaged)
|
||||||
return;
|
collection.PerformWrite(c => c.Name = textBox.Current.Value);
|
||||||
|
else if (!string.IsNullOrEmpty(textBox.Current.Value))
|
||||||
|
realm.Write(r => r.Add(new BeatmapCollection(textBox.Current.Value)));
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(collectionName.Value))
|
textBox.Text = string.Empty;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,22 +127,17 @@ namespace osu.Game.Collections
|
|||||||
|
|
||||||
public class DeleteButton : CompositeDrawable
|
public class DeleteButton : CompositeDrawable
|
||||||
{
|
{
|
||||||
public readonly IBindable<bool> IsCreated = new Bindable<bool>();
|
public Func<Vector2, bool> IsTextBoxHovered = null!;
|
||||||
|
|
||||||
public Func<Vector2, bool> IsTextBoxHovered;
|
[Resolved]
|
||||||
|
private IDialogOverlay? dialogOverlay { get; set; }
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
private readonly Live<BeatmapCollection> collection;
|
||||||
private IDialogOverlay dialogOverlay { get; set; }
|
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
private Drawable fadeContainer = null!;
|
||||||
private CollectionManager collectionManager { get; set; }
|
private Drawable background = null!;
|
||||||
|
|
||||||
private readonly BeatmapCollection collection;
|
public DeleteButton(Live<BeatmapCollection> collection)
|
||||||
|
|
||||||
private Drawable fadeContainer;
|
|
||||||
private Drawable background;
|
|
||||||
|
|
||||||
public DeleteButton(BeatmapCollection collection)
|
|
||||||
{
|
{
|
||||||
this.collection = collection;
|
this.collection = collection;
|
||||||
RelativeSizeAxes = Axes.Y;
|
RelativeSizeAxes = Axes.Y;
|
||||||
@ -204,12 +171,6 @@ namespace osu.Game.Collections
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos);
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
@ -227,7 +188,7 @@ namespace osu.Game.Collections
|
|||||||
{
|
{
|
||||||
background.FlashColour(Color4.White, 150);
|
background.FlashColour(Color4.White, 150);
|
||||||
|
|
||||||
if (collection.BeatmapHashes.Count == 0)
|
if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0)
|
||||||
deleteCollection();
|
deleteCollection();
|
||||||
else
|
else
|
||||||
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
|
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
|
||||||
@ -235,7 +196,7 @@ namespace osu.Game.Collections
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteCollection() => collectionManager?.Collections.Remove(collection);
|
private void deleteCollection() => collection.PerformWrite(c => c.Realm.Remove(c));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
@ -24,10 +21,7 @@ namespace osu.Game.Collections
|
|||||||
private const double enter_duration = 500;
|
private const double enter_duration = 500;
|
||||||
private const double exit_duration = 200;
|
private const double exit_duration = 200;
|
||||||
|
|
||||||
private AudioFilter lowPassFilter;
|
private AudioFilter lowPassFilter = null!;
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
|
||||||
private CollectionManager collectionManager { get; set; }
|
|
||||||
|
|
||||||
public ManageCollectionsDialog()
|
public ManageCollectionsDialog()
|
||||||
{
|
{
|
||||||
@ -107,7 +101,6 @@ namespace osu.Game.Collections
|
|||||||
new DrawableCollectionList
|
new DrawableCollectionList
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Items = { BindTarget = collectionManager?.Collections ?? new BindableList<BeatmapCollection>() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
169
osu.Game/Database/LegacyCollectionImporter.cs
Normal file
169
osu.Game/Database/LegacyCollectionImporter.cs
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Game.Collections;
|
||||||
|
using osu.Game.IO.Legacy;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
public class LegacyCollectionImporter
|
||||||
|
{
|
||||||
|
public Action<Notification>? PostNotification { protected get; set; }
|
||||||
|
|
||||||
|
private readonly RealmAccess realm;
|
||||||
|
|
||||||
|
private const string database_name = "collection.db";
|
||||||
|
|
||||||
|
public LegacyCollectionImporter(RealmAccess realm)
|
||||||
|
{
|
||||||
|
this.realm = realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<int> GetAvailableCount(Storage storage)
|
||||||
|
{
|
||||||
|
if (!storage.Exists(database_name))
|
||||||
|
return Task.FromResult(0);
|
||||||
|
|
||||||
|
return Task.Run(() =>
|
||||||
|
{
|
||||||
|
using (var stream = storage.GetStream(database_name))
|
||||||
|
return readCollections(stream).Count;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 ImportFromStorage(Storage storage)
|
||||||
|
{
|
||||||
|
if (!storage.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 = storage.GetStream(database_name))
|
||||||
|
await Import(stream).ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Import(Stream stream)
|
||||||
|
{
|
||||||
|
var notification = new ProgressNotification
|
||||||
|
{
|
||||||
|
State = ProgressNotificationState.Active,
|
||||||
|
Text = "Collections import is initialising..."
|
||||||
|
};
|
||||||
|
|
||||||
|
PostNotification?.Invoke(notification);
|
||||||
|
|
||||||
|
var importedCollections = readCollections(stream, notification);
|
||||||
|
await importCollections(importedCollections).ConfigureAwait(false);
|
||||||
|
|
||||||
|
notification.CompletionText = $"Imported {importedCollections.Count} collections";
|
||||||
|
notification.State = ProgressNotificationState.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task importCollections(List<BeatmapCollection> newCollections)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
realm.Write(r =>
|
||||||
|
{
|
||||||
|
foreach (var collection in newCollections)
|
||||||
|
{
|
||||||
|
var existing = r.All<BeatmapCollection>().FirstOrDefault(c => c.Name == collection.Name);
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
foreach (string newBeatmap in existing.BeatmapMD5Hashes)
|
||||||
|
{
|
||||||
|
if (!existing.BeatmapMD5Hashes.Contains(newBeatmap))
|
||||||
|
existing.BeatmapMD5Hashes.Add(newBeatmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
r.Add(collection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tcs.SetResult(true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, "Failed to import collection.");
|
||||||
|
tcs.SetException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(sr.ReadString());
|
||||||
|
int mapCount = sr.ReadInt32();
|
||||||
|
|
||||||
|
for (int j = 0; j < mapCount; j++)
|
||||||
|
{
|
||||||
|
if (notification?.CancellationToken.IsCancellationRequested == true)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
string checksum = sr.ReadString();
|
||||||
|
|
||||||
|
collection.BeatmapMD5Hashes.Add(checksum);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,6 @@ using osu.Framework.Extensions.EnumExtensions;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Collections;
|
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Settings.Sections.Maintenance;
|
using osu.Game.Overlays.Settings.Sections.Maintenance;
|
||||||
@ -36,15 +35,15 @@ namespace osu.Game.Database
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private ScoreManager scores { get; set; }
|
private ScoreManager scores { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private CollectionManager collections { get; set; }
|
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private OsuGame game { get; set; }
|
private OsuGame game { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IDialogOverlay dialogOverlay { get; set; }
|
private IDialogOverlay dialogOverlay { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private RealmAccess realmAccess { get; set; }
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private DesktopGameHost desktopGameHost { get; set; }
|
private DesktopGameHost desktopGameHost { get; set; }
|
||||||
|
|
||||||
@ -72,7 +71,7 @@ namespace osu.Game.Database
|
|||||||
return await new LegacySkinImporter(skins).GetAvailableCount(stableStorage);
|
return await new LegacySkinImporter(skins).GetAvailableCount(stableStorage);
|
||||||
|
|
||||||
case StableContent.Collections:
|
case StableContent.Collections:
|
||||||
return await collections.GetAvailableCount(stableStorage);
|
return await new LegacyCollectionImporter(realmAccess).GetAvailableCount(stableStorage);
|
||||||
|
|
||||||
case StableContent.Scores:
|
case StableContent.Scores:
|
||||||
return await new LegacyScoreImporter(scores).GetAvailableCount(stableStorage);
|
return await new LegacyScoreImporter(scores).GetAvailableCount(stableStorage);
|
||||||
@ -109,7 +108,7 @@ namespace osu.Game.Database
|
|||||||
importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage));
|
importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage));
|
||||||
|
|
||||||
if (content.HasFlagFast(StableContent.Collections))
|
if (content.HasFlagFast(StableContent.Collections))
|
||||||
importTasks.Add(beatmapImportTask.ContinueWith(_ => collections.ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
|
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess).ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
|
||||||
|
|
||||||
if (content.HasFlagFast(StableContent.Scores))
|
if (content.HasFlagFast(StableContent.Scores))
|
||||||
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
|
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
|
||||||
|
@ -14,6 +14,7 @@ using System.Threading.Tasks;
|
|||||||
using osu.Framework;
|
using osu.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Development;
|
using osu.Framework.Development;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
@ -64,8 +65,9 @@ namespace osu.Game.Database
|
|||||||
/// 18 2022-07-19 Added OnlineMD5Hash and LastOnlineUpdate to BeatmapInfo.
|
/// 18 2022-07-19 Added OnlineMD5Hash and LastOnlineUpdate to BeatmapInfo.
|
||||||
/// 19 2022-07-19 Added DateSubmitted and DateRanked to BeatmapSetInfo.
|
/// 19 2022-07-19 Added DateSubmitted and DateRanked to BeatmapSetInfo.
|
||||||
/// 20 2022-07-21 Added LastAppliedDifficultyVersion to RulesetInfo, changed default value of BeatmapInfo.StarRating to -1.
|
/// 20 2022-07-21 Added LastAppliedDifficultyVersion to RulesetInfo, changed default value of BeatmapInfo.StarRating to -1.
|
||||||
|
/// 21 2022-07-27 Migrate collections to realm (BeatmapCollection).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int schema_version = 20;
|
private const int schema_version = 21;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||||
@ -790,6 +792,27 @@ namespace osu.Game.Database
|
|||||||
beatmap.StarRating = -1;
|
beatmap.StarRating = -1;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 21:
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Migrate collections from external file to inside realm.
|
||||||
|
// We use the "legacy" importer because that is how things were actually being saved out until now.
|
||||||
|
var legacyCollectionImporter = new LegacyCollectionImporter(this);
|
||||||
|
|
||||||
|
if (legacyCollectionImporter.GetAvailableCount(storage).GetResultSafely() > 0)
|
||||||
|
{
|
||||||
|
legacyCollectionImporter.ImportFromStorage(storage);
|
||||||
|
storage.Delete("collection.db");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// can be removed 20221027 (just for initial safety).
|
||||||
|
Logger.Error(e, "Collections could not be migrated to realm. Please provide your \"collection.db\" to the dev team.");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -858,11 +858,6 @@ 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),
|
|
||||||
}, Add, true);
|
|
||||||
|
|
||||||
loadComponentSingleFile(legacyImportManager, Add);
|
loadComponentSingleFile(legacyImportManager, Add);
|
||||||
|
|
||||||
loadComponentSingleFile(screenshotManager, Add);
|
loadComponentSingleFile(screenshotManager, Add);
|
||||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Music
|
|||||||
public Action<FilterCriteria> FilterChanged;
|
public Action<FilterCriteria> FilterChanged;
|
||||||
|
|
||||||
public readonly FilterTextBox Search;
|
public readonly FilterTextBox Search;
|
||||||
private readonly CollectionDropdown collectionDropdown;
|
private readonly NowPlayingCollectionDropdown collectionDropdown;
|
||||||
|
|
||||||
public FilterControl()
|
public FilterControl()
|
||||||
{
|
{
|
||||||
@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Music
|
|||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 40,
|
Height = 40,
|
||||||
},
|
},
|
||||||
collectionDropdown = new CollectionDropdown { RelativeSizeAxes = Axes.X }
|
collectionDropdown = new NowPlayingCollectionDropdown { RelativeSizeAxes = Axes.X }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Game.Collections;
|
using osu.Game.Collections;
|
||||||
|
using osu.Game.Database;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Music
|
namespace osu.Game.Overlays.Music
|
||||||
{
|
{
|
||||||
@ -19,6 +20,6 @@ namespace osu.Game.Overlays.Music
|
|||||||
/// The collection to filter beatmaps from.
|
/// The collection to filter beatmaps from.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
public BeatmapCollection Collection;
|
public Live<BeatmapCollection> Collection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,9 @@ using osu.Game.Graphics;
|
|||||||
namespace osu.Game.Overlays.Music
|
namespace osu.Game.Overlays.Music
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A <see cref="CollectionFilterDropdown"/> for use in the <see cref="NowPlayingOverlay"/>.
|
/// A <see cref="CollectionDropdown"/> for use in the <see cref="NowPlayingOverlay"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CollectionDropdown : CollectionFilterDropdown
|
public class NowPlayingCollectionDropdown : CollectionDropdown
|
||||||
{
|
{
|
||||||
protected override bool ShowManageCollectionsItem => false;
|
protected override bool ShowManageCollectionsItem => false;
|
||||||
|
|
@ -31,14 +31,16 @@ namespace osu.Game.Overlays.Music
|
|||||||
{
|
{
|
||||||
var items = (SearchContainer<RearrangeableListItem<Live<BeatmapSetInfo>>>)ListContainer;
|
var items = (SearchContainer<RearrangeableListItem<Live<BeatmapSetInfo>>>)ListContainer;
|
||||||
|
|
||||||
|
string[] currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray());
|
||||||
|
|
||||||
foreach (var item in items.OfType<PlaylistItem>())
|
foreach (var item in items.OfType<PlaylistItem>())
|
||||||
{
|
{
|
||||||
if (criteria.Collection == null)
|
if (currentCollectionHashes == null)
|
||||||
item.InSelectedCollection = true;
|
item.InSelectedCollection = true;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
item.InSelectedCollection = item.Model.Value.Beatmaps.Select(b => b.MD5Hash)
|
item.InSelectedCollection = item.Model.Value.Beatmaps.Select(b => b.MD5Hash)
|
||||||
.Any(criteria.Collection.BeatmapHashes.Contains);
|
.Any(currentCollectionHashes.Contains);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ using osu.Framework.Localisation;
|
|||||||
using osu.Game.Collections;
|
using osu.Game.Collections;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||||
{
|
{
|
||||||
@ -15,11 +16,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
|||||||
|
|
||||||
private SettingsButton importCollectionsButton = null!;
|
private SettingsButton importCollectionsButton = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[Resolved]
|
||||||
private void load(CollectionManager? collectionManager, LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay)
|
private RealmAccess realm { get; set; } = null!;
|
||||||
{
|
|
||||||
if (collectionManager == null) return;
|
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private INotificationOverlay? notificationOverlay { get; set; }
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay)
|
||||||
|
{
|
||||||
if (legacyImportManager?.SupportsImportFromStable == true)
|
if (legacyImportManager?.SupportsImportFromStable == true)
|
||||||
{
|
{
|
||||||
Add(importCollectionsButton = new SettingsButton
|
Add(importCollectionsButton = new SettingsButton
|
||||||
@ -38,9 +43,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
|||||||
Text = MaintenanceSettingsStrings.DeleteAllCollections,
|
Text = MaintenanceSettingsStrings.DeleteAllCollections,
|
||||||
Action = () =>
|
Action = () =>
|
||||||
{
|
{
|
||||||
dialogOverlay?.Push(new MassDeleteConfirmationDialog(collectionManager.DeleteAll));
|
dialogOverlay?.Push(new MassDeleteConfirmationDialog(deleteAllCollections));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void deleteAllCollections()
|
||||||
|
{
|
||||||
|
realm.Write(r => r.RemoveAll<BeatmapCollection>());
|
||||||
|
notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all collections!" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,6 +94,9 @@ namespace osu.Game.Screens.OnlinePlay
|
|||||||
private PanelBackground panelBackground;
|
private PanelBackground panelBackground;
|
||||||
private FillFlowContainer mainFillFlow;
|
private FillFlowContainer mainFillFlow;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private RealmAccess realm { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private RulesetStore rulesets { get; set; }
|
private RulesetStore rulesets { get; set; }
|
||||||
|
|
||||||
@ -112,9 +115,6 @@ namespace osu.Game.Screens.OnlinePlay
|
|||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private BeatmapSetOverlay beatmapOverlay { get; set; }
|
private BeatmapSetOverlay beatmapOverlay { get; set; }
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
|
||||||
private CollectionManager collectionManager { get; set; }
|
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
||||||
|
|
||||||
@ -495,11 +495,11 @@ namespace osu.Game.Screens.OnlinePlay
|
|||||||
if (beatmapOverlay != null)
|
if (beatmapOverlay != null)
|
||||||
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(Item.Beatmap.OnlineID)));
|
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(Item.Beatmap.OnlineID)));
|
||||||
|
|
||||||
if (collectionManager != null && beatmap != null)
|
if (beatmap != null)
|
||||||
{
|
{
|
||||||
if (beatmaps.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID) is BeatmapInfo local && !local.BeatmapSet.AsNonNull().DeletePending)
|
if (beatmaps.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID) is BeatmapInfo local && !local.BeatmapSet.AsNonNull().DeletePending)
|
||||||
{
|
{
|
||||||
var collectionItems = collectionManager.Collections.Select(c => new CollectionToggleMenuItem(c, beatmap)).Cast<OsuMenuItem>().ToList();
|
var collectionItems = realm.Realm.All<BeatmapCollection>().AsEnumerable().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast<OsuMenuItem>().ToList();
|
||||||
if (manageCollectionsDialog != null)
|
if (manageCollectionsDialog != null)
|
||||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (match)
|
if (match)
|
||||||
match &= criteria.Collection?.BeatmapHashes.Contains(BeatmapInfo.MD5Hash) ?? true;
|
match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true;
|
||||||
|
|
||||||
if (match && criteria.RulesetCriteria != null)
|
if (match && criteria.RulesetCriteria != null)
|
||||||
match &= criteria.RulesetCriteria.Matches(BeatmapInfo);
|
match &= criteria.RulesetCriteria.Matches(BeatmapInfo);
|
||||||
|
@ -22,6 +22,7 @@ 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.Collections;
|
||||||
|
using osu.Game.Database;
|
||||||
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;
|
||||||
@ -63,12 +64,12 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private BeatmapDifficultyCache difficultyCache { get; set; }
|
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
|
||||||
private CollectionManager collectionManager { get; set; }
|
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private RealmAccess realm { get; set; }
|
||||||
|
|
||||||
private IBindable<StarDifficulty?> starDifficultyBindable;
|
private IBindable<StarDifficulty?> starDifficultyBindable;
|
||||||
private CancellationTokenSource starDifficultyCancellationSource;
|
private CancellationTokenSource starDifficultyCancellationSource;
|
||||||
|
|
||||||
@ -237,14 +238,11 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null)
|
if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null)
|
||||||
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID)));
|
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID)));
|
||||||
|
|
||||||
if (collectionManager != null)
|
var collectionItems = realm.Realm.All<BeatmapCollection>().AsEnumerable().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast<OsuMenuItem>().ToList();
|
||||||
{
|
if (manageCollectionsDialog != null)
|
||||||
var collectionItems = collectionManager.Collections.Select(c => new CollectionToggleMenuItem(c, beatmapInfo)).Cast<OsuMenuItem>().ToList();
|
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||||
if (manageCollectionsDialog != null)
|
|
||||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
|
||||||
|
|
||||||
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
|
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
|
||||||
}
|
|
||||||
|
|
||||||
if (hideRequested != null)
|
if (hideRequested != null)
|
||||||
items.Add(new OsuMenuItem(CommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo)));
|
items.Add(new OsuMenuItem(CommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo)));
|
||||||
|
@ -17,6 +17,7 @@ using osu.Framework.Graphics.UserInterface;
|
|||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Collections;
|
using osu.Game.Collections;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
|
||||||
@ -32,12 +33,12 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private IDialogOverlay dialogOverlay { get; set; }
|
private IDialogOverlay dialogOverlay { get; set; }
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
|
||||||
private CollectionManager collectionManager { get; set; }
|
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private RealmAccess realm { get; set; }
|
||||||
|
|
||||||
public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty<DrawableCarouselItem>() : beatmapContainer.AliveChildren;
|
public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty<DrawableCarouselItem>() : beatmapContainer.AliveChildren;
|
||||||
|
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
@ -223,14 +224,11 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
if (beatmapSet.OnlineID > 0 && viewDetails != null)
|
if (beatmapSet.OnlineID > 0 && viewDetails != null)
|
||||||
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID)));
|
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID)));
|
||||||
|
|
||||||
if (collectionManager != null)
|
var collectionItems = realm.Realm.All<BeatmapCollection>().AsEnumerable().Select(createCollectionMenuItem).ToList();
|
||||||
{
|
if (manageCollectionsDialog != null)
|
||||||
var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList();
|
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||||
if (manageCollectionsDialog != null)
|
|
||||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
|
||||||
|
|
||||||
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
|
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)));
|
||||||
@ -247,7 +245,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
|
|
||||||
TernaryState state;
|
TernaryState state;
|
||||||
|
|
||||||
int countExisting = beatmapSet.Beatmaps.Count(b => collection.BeatmapHashes.Contains(b.MD5Hash));
|
int countExisting = beatmapSet.Beatmaps.Count(b => collection.BeatmapMD5Hashes.Contains(b.MD5Hash));
|
||||||
|
|
||||||
if (countExisting == beatmapSet.Beatmaps.Count)
|
if (countExisting == beatmapSet.Beatmaps.Count)
|
||||||
state = TernaryState.True;
|
state = TernaryState.True;
|
||||||
@ -256,24 +254,29 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
else
|
else
|
||||||
state = TernaryState.False;
|
state = TernaryState.False;
|
||||||
|
|
||||||
return new TernaryStateToggleMenuItem(collection.Name.Value, MenuItemType.Standard, s =>
|
var liveCollection = collection.ToLive(realm);
|
||||||
|
|
||||||
|
return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s =>
|
||||||
{
|
{
|
||||||
foreach (var b in beatmapSet.Beatmaps)
|
liveCollection.PerformWrite(c =>
|
||||||
{
|
{
|
||||||
switch (s)
|
foreach (var b in beatmapSet.Beatmaps)
|
||||||
{
|
{
|
||||||
case TernaryState.True:
|
switch (s)
|
||||||
if (collection.BeatmapHashes.Contains(b.MD5Hash))
|
{
|
||||||
continue;
|
case TernaryState.True:
|
||||||
|
if (c.BeatmapMD5Hashes.Contains(b.MD5Hash))
|
||||||
|
continue;
|
||||||
|
|
||||||
collection.BeatmapHashes.Add(b.MD5Hash);
|
c.BeatmapMD5Hashes.Add(b.MD5Hash);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TernaryState.False:
|
case TernaryState.False:
|
||||||
collection.BeatmapHashes.Remove(b.MD5Hash);
|
c.BeatmapMD5Hashes.Remove(b.MD5Hash);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
State = { Value = state }
|
State = { Value = state }
|
||||||
|
@ -39,6 +39,10 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
private Bindable<GroupMode> groupMode;
|
private Bindable<GroupMode> groupMode;
|
||||||
|
|
||||||
|
private SeekLimitedSearchTextBox searchTextBox;
|
||||||
|
|
||||||
|
private CollectionDropdown collectionDropdown;
|
||||||
|
|
||||||
public FilterCriteria CreateCriteria()
|
public FilterCriteria CreateCriteria()
|
||||||
{
|
{
|
||||||
string query = searchTextBox.Text;
|
string query = searchTextBox.Text;
|
||||||
@ -49,7 +53,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
|
CollectionBeatmapMD5Hashes = collectionDropdown.Current.Value?.Collection?.PerformRead(c => c.BeatmapMD5Hashes)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!minimumStars.IsDefault)
|
if (!minimumStars.IsDefault)
|
||||||
@ -64,10 +68,6 @@ namespace osu.Game.Screens.Select
|
|||||||
return criteria;
|
return criteria;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@ -179,10 +179,11 @@ namespace osu.Game.Screens.Select
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 0.48f,
|
Width = 0.48f,
|
||||||
},
|
},
|
||||||
collectionDropdown = new CollectionFilterDropdown
|
collectionDropdown = new CollectionDropdown
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopRight,
|
Anchor = Anchor.TopRight,
|
||||||
Origin = Anchor.TopRight,
|
Origin = Anchor.TopRight,
|
||||||
|
RequestFilter = updateCriteria,
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Y = 4,
|
Y = 4,
|
||||||
Width = 0.5f,
|
Width = 0.5f,
|
||||||
@ -209,15 +210,6 @@ namespace osu.Game.Screens.Select
|
|||||||
groupMode.BindValueChanged(_ => updateCriteria());
|
groupMode.BindValueChanged(_ => updateCriteria());
|
||||||
sortMode.BindValueChanged(_ => updateCriteria());
|
sortMode.BindValueChanged(_ => updateCriteria());
|
||||||
|
|
||||||
collectionDropdown.Current.ValueChanged += val =>
|
|
||||||
{
|
|
||||||
if (val.NewValue == null)
|
|
||||||
// may be null briefly while menu is repopulated.
|
|
||||||
return;
|
|
||||||
|
|
||||||
updateCriteria();
|
|
||||||
};
|
|
||||||
|
|
||||||
searchTextBox.Current.ValueChanged += _ => updateCriteria();
|
searchTextBox.Current.ValueChanged += _ => updateCriteria();
|
||||||
|
|
||||||
updateCriteria();
|
updateCriteria();
|
||||||
|
@ -68,10 +68,10 @@ namespace osu.Game.Screens.Select
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The collection to filter beatmaps from.
|
/// Hashes from the <see cref="BeatmapCollection"/> to filter to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
public BeatmapCollection Collection;
|
public IEnumerable<string> CollectionBeatmapMD5Hashes { get; set; }
|
||||||
|
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
public IRulesetFilterCriteria RulesetCriteria { get; set; }
|
public IRulesetFilterCriteria RulesetCriteria { get; set; }
|
||||||
|
Loading…
Reference in New Issue
Block a user