mirror of
https://github.com/ppy/osu.git
synced 2025-02-15 06:13:03 +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.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Collections.IO
|
||||
@ -29,7 +32,11 @@ namespace osu.Game.Tests.Collections.IO
|
||||
|
||||
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
|
||||
{
|
||||
@ -49,18 +56,22 @@ namespace osu.Game.Tests.Collections.IO
|
||||
|
||||
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.
|
||||
// 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
|
||||
// and have them associate with collections if/when they become available.
|
||||
// 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,
|
||||
// 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.
|
||||
|
||||
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
|
||||
Assert.That(osu.CollectionManager.Collections[0].BeatmapHashes.Count, Is.EqualTo(1));
|
||||
Assert.That(collections[0].Name, Is.EqualTo("First"));
|
||||
Assert.That(collections[0].BeatmapMD5Hashes.Count, Is.EqualTo(1));
|
||||
|
||||
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
|
||||
Assert.That(osu.CollectionManager.Collections[1].BeatmapHashes.Count, Is.EqualTo(12));
|
||||
Assert.That(collections[1].Name, Is.EqualTo("Second"));
|
||||
Assert.That(collections[1].BeatmapMD5Hashes.Count, Is.EqualTo(12));
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -80,13 +91,18 @@ namespace osu.Game.Tests.Collections.IO
|
||||
|
||||
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(osu.CollectionManager.Collections[0].BeatmapHashes.Count, Is.EqualTo(1));
|
||||
Assert.That(collections.Count, Is.EqualTo(2));
|
||||
|
||||
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
|
||||
Assert.That(osu.CollectionManager.Collections[1].BeatmapHashes.Count, Is.EqualTo(12));
|
||||
Assert.That(collections[0].Name, Is.EqualTo("First"));
|
||||
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
|
||||
{
|
||||
@ -123,7 +139,11 @@ namespace osu.Game.Tests.Collections.IO
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
@ -148,12 +168,18 @@ namespace osu.Game.Tests.Collections.IO
|
||||
|
||||
await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
|
||||
|
||||
// Move first beatmap from second collection into the first.
|
||||
osu.CollectionManager.Collections[0].BeatmapHashes.Add(osu.CollectionManager.Collections[1].BeatmapHashes[0]);
|
||||
osu.CollectionManager.Collections[1].BeatmapHashes.RemoveAt(0);
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
osu.Realm.Write(realm =>
|
||||
{
|
||||
var collections = realm.All<BeatmapCollection>().ToList();
|
||||
|
||||
// Rename the second collecction.
|
||||
osu.CollectionManager.Collections[1].Name.Value = "Another";
|
||||
// Move first beatmap from second collection into the first.
|
||||
collections[0].BeatmapMD5Hashes.Add(collections[1].BeatmapMD5Hashes[0]);
|
||||
collections[1].BeatmapMD5Hashes.RemoveAt(0);
|
||||
|
||||
// Rename the second collecction.
|
||||
collections[1].Name = "Another";
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -168,13 +194,17 @@ namespace osu.Game.Tests.Collections.IO
|
||||
{
|
||||
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(osu.CollectionManager.Collections[0].BeatmapHashes.Count, Is.EqualTo(2));
|
||||
Assert.That(collections[0].Name, Is.EqualTo("First"));
|
||||
Assert.That(collections[0].BeatmapMD5Hashes.Count, Is.EqualTo(2));
|
||||
|
||||
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Another"));
|
||||
Assert.That(osu.CollectionManager.Collections[1].BeatmapHashes.Count, Is.EqualTo(11));
|
||||
Assert.That(collections[1].Name, Is.EqualTo("Another"));
|
||||
Assert.That(collections[1].BeatmapMD5Hashes.Count, Is.EqualTo(11));
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -187,7 +217,7 @@ namespace osu.Game.Tests.Collections.IO
|
||||
{
|
||||
// intentionally spin this up on a separate task to avoid disposal deadlocks.
|
||||
// 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.Extensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests
|
||||
@ -47,7 +47,7 @@ namespace osu.Game.Tests
|
||||
|
||||
public class TestOsuGameBase : OsuGameBase
|
||||
{
|
||||
public CollectionManager CollectionManager { get; private set; }
|
||||
public RealmAccess Realm => Dependencies.Get<RealmAccess>();
|
||||
|
||||
private readonly bool withBeatmap;
|
||||
|
||||
@ -62,8 +62,6 @@ namespace osu.Game.Tests
|
||||
// Beatmap must be imported before the collection manager is loaded.
|
||||
if (withBeatmap)
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
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 };
|
||||
|
||||
private DialogOverlay dialogOverlay;
|
||||
private CollectionManager manager;
|
||||
|
||||
private BeatmapManager beatmapManager;
|
||||
|
||||
private ManageCollectionsDialog dialog;
|
||||
private DialogOverlay dialogOverlay = null!;
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
private ManageCollectionsDialog dialog = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
@ -45,19 +40,17 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
|
||||
base.Content.AddRange(new Drawable[]
|
||||
{
|
||||
manager = new CollectionManager(LocalStorage),
|
||||
Content,
|
||||
dialogOverlay = new DialogOverlay(),
|
||||
});
|
||||
|
||||
Dependencies.Cache(manager);
|
||||
Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
manager.Collections.Clear();
|
||||
Realm.Write(r => r.RemoveAll<BeatmapCollection>());
|
||||
Child = dialog = new ManageCollectionsDialog();
|
||||
});
|
||||
|
||||
@ -77,17 +70,17 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
[Test]
|
||||
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]
|
||||
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);
|
||||
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);
|
||||
assertCollectionName(1, "Second collection");
|
||||
}
|
||||
@ -107,7 +100,7 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
[Test]
|
||||
public void TestAddCollectionViaPlaceholder()
|
||||
{
|
||||
DrawableCollectionListItem placeholderItem = null;
|
||||
DrawableCollectionListItem placeholderItem = null!;
|
||||
|
||||
AddStep("focus placeholder", () =>
|
||||
{
|
||||
@ -115,24 +108,37 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
// Done directly via the collection since InputManager methods cannot add text to textbox...
|
||||
AddStep("change collection name", () => placeholderItem.Model.Name.Value = "a");
|
||||
assertCollectionCount(1);
|
||||
AddAssert("collection now exists", () => manager.Collections.Contains(placeholderItem.Model));
|
||||
assertCollectionCount(0);
|
||||
|
||||
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]
|
||||
public void TestRemoveCollectionExternal()
|
||||
{
|
||||
AddStep("add two collections", () => manager.Collections.AddRange(new[]
|
||||
{
|
||||
new BeatmapCollection { Name = { Value = "1" } },
|
||||
new BeatmapCollection { Name = { Value = "2" } },
|
||||
}));
|
||||
BeatmapCollection first = null!;
|
||||
|
||||
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);
|
||||
assertCollectionName(0, "2");
|
||||
}
|
||||
@ -142,7 +148,7 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
{
|
||||
AddStep("add dropdown", () =>
|
||||
{
|
||||
Add(new CollectionFilterDropdown
|
||||
Add(new CollectionDropdown
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
@ -150,21 +156,27 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
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 = { Value = "1" }, BeatmapHashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } },
|
||||
}));
|
||||
new BeatmapCollection(name: "1"),
|
||||
new BeatmapCollection(name: "1")
|
||||
{
|
||||
BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash }
|
||||
},
|
||||
})));
|
||||
}
|
||||
|
||||
[Test]
|
||||
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 = { Value = "2" }, BeatmapHashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } },
|
||||
}));
|
||||
new BeatmapCollection(name: "1"),
|
||||
new BeatmapCollection(name: "2")
|
||||
{
|
||||
BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash }
|
||||
},
|
||||
})));
|
||||
|
||||
assertCollectionCount(2);
|
||||
|
||||
@ -197,10 +209,13 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
[Test]
|
||||
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);
|
||||
|
||||
@ -223,34 +238,67 @@ namespace osu.Game.Tests.Visual.Collections
|
||||
[Test]
|
||||
public void TestCollectionRenamedExternal()
|
||||
{
|
||||
AddStep("add two collections", () => manager.Collections.AddRange(new[]
|
||||
BeatmapCollection first = null!;
|
||||
|
||||
AddStep("add two collections", () =>
|
||||
{
|
||||
new BeatmapCollection { Name = { Value = "1" } },
|
||||
new BeatmapCollection { Name = { Value = "2" } },
|
||||
}));
|
||||
Realm.Write(r =>
|
||||
{
|
||||
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]
|
||||
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" } },
|
||||
new BeatmapCollection { Name = { Value = "2" } },
|
||||
}));
|
||||
Realm.Write(r =>
|
||||
{
|
||||
r.Add(new[]
|
||||
{
|
||||
first = new BeatmapCollection(name: "1"),
|
||||
new BeatmapCollection(name: "2"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
assertCollectionCount(2);
|
||||
|
||||
AddStep("change first collection name", () => dialog.ChildrenOfType<TextBox>().First().Text = "First");
|
||||
AddAssert("collection has new name", () => manager.Collections[0].Name.Value == "First");
|
||||
AddStep("focus first collection", () =>
|
||||
{
|
||||
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)
|
||||
=> 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)
|
||||
=> 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("open collections dropdown", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(songSelect.ChildrenOfType<CollectionFilterDropdown>().Single());
|
||||
InputManager.MoveMouseTo(songSelect.ChildrenOfType<CollectionDropdown>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("press back once", () => InputManager.Click(MouseButton.Button1));
|
||||
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
|
||||
AddAssert("collections dropdown closed", () => songSelect
|
||||
.ChildrenOfType<CollectionFilterDropdown>().Single()
|
||||
.ChildrenOfType<CollectionDropdown>().Single()
|
||||
.ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu>().Single().State == MenuState.Closed);
|
||||
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@ -28,11 +26,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private CollectionManager collectionManager;
|
||||
|
||||
private BeatmapManager beatmapManager;
|
||||
|
||||
private FilterControl control;
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
private FilterControl control = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
@ -45,17 +40,14 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
base.Content.AddRange(new Drawable[]
|
||||
{
|
||||
collectionManager = new CollectionManager(LocalStorage),
|
||||
Content
|
||||
});
|
||||
|
||||
Dependencies.Cache(collectionManager);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
collectionManager.Collections.Clear();
|
||||
Realm.Write(r => r.RemoveAll<BeatmapCollection>());
|
||||
|
||||
Child = control = new FilterControl
|
||||
{
|
||||
@ -76,8 +68,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestCollectionAddedToDropdown()
|
||||
{
|
||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } }));
|
||||
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
assertCollectionDropdownContains("1");
|
||||
assertCollectionDropdownContains("2");
|
||||
}
|
||||
@ -85,9 +77,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestCollectionRemovedFromDropdown()
|
||||
{
|
||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
|
||||
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } }));
|
||||
AddStep("remove collection", () => collectionManager.Collections.RemoveAt(0));
|
||||
BeatmapCollection first = null!;
|
||||
|
||||
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("2");
|
||||
@ -96,16 +90,16 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
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", () =>
|
||||
{
|
||||
var dropdown = control.ChildrenOfType<CollectionFilterDropdown>().Single();
|
||||
var dropdown = control.ChildrenOfType<CollectionDropdown>().Single();
|
||||
dropdown.Current.Value = dropdown.ItemSource.ElementAt(1);
|
||||
});
|
||||
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("change name", () => collectionManager.Collections[0].Name.Value = "First");
|
||||
AddStep("change name", () => Realm.Write(_ => getFirstCollection().Name = "First"));
|
||||
|
||||
assertCollectionDropdownContains("First");
|
||||
assertCollectionHeaderDisplays("First");
|
||||
@ -123,7 +117,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
public void TestCollectionFilterHasAddButton()
|
||||
{
|
||||
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)));
|
||||
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
|
||||
}
|
||||
@ -133,7 +127,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
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]));
|
||||
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("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));
|
||||
|
||||
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));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@ -166,24 +160,26 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
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));
|
||||
|
||||
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));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManageCollectionsFilterIsNotSelected()
|
||||
{
|
||||
bool received = false;
|
||||
|
||||
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", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1));
|
||||
@ -192,18 +188,28 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
addExpandHeaderStep();
|
||||
|
||||
AddStep("watch for filter requests", () =>
|
||||
{
|
||||
received = false;
|
||||
control.ChildrenOfType<CollectionDropdown>().First().RequestFilter = () => received = true;
|
||||
});
|
||||
|
||||
AddStep("click manage collections filter", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getCollectionDropdownItems().Last());
|
||||
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)
|
||||
=> 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) =>
|
||||
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", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(control.ChildrenOfType<CollectionFilterDropdown.CollectionDropdownHeader>().Single());
|
||||
InputManager.MoveMouseTo(control.ChildrenOfType<CollectionDropdown.CollectionDropdownHeader>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
@ -226,6 +232,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of beatmaps grouped by a name.
|
||||
/// </summary>
|
||||
public class BeatmapCollection
|
||||
public class BeatmapCollection : RealmObject, IHasGuidPrimaryKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked whenever any change occurs on this <see cref="BeatmapCollection"/>.
|
||||
/// </summary>
|
||||
public event Action Changed;
|
||||
[PrimaryKey]
|
||||
public Guid ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The collection's name.
|
||||
/// </summary>
|
||||
public readonly Bindable<string> Name = new Bindable<string>();
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="BeatmapInfo.MD5Hash"/>es of beatmaps contained by the collection.
|
||||
/// </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>
|
||||
/// The date when this collection was last modified.
|
||||
/// </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();
|
||||
Name.ValueChanged += _ => onChange();
|
||||
ID = Guid.NewGuid();
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Database;
|
||||
|
||||
namespace osu.Game.Collections
|
||||
{
|
||||
@ -18,26 +15,29 @@ namespace osu.Game.Collections
|
||||
/// The collection to filter beatmaps from.
|
||||
/// May be null to not filter by collection (include all beatmaps).
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public readonly BeatmapCollection Collection;
|
||||
public readonly Live<BeatmapCollection>? Collection;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the collection.
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public readonly Bindable<string> CollectionName;
|
||||
public string CollectionName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="CollectionFilterMenuItem"/>.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
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)
|
||||
return false;
|
||||
@ -45,20 +45,20 @@ namespace osu.Game.Collections
|
||||
// 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.
|
||||
if (Collection != null)
|
||||
return Collection == other.Collection;
|
||||
return Collection.ID == other.Collection?.ID;
|
||||
|
||||
// fallback to name-based comparison.
|
||||
// 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 AllBeatmapsCollectionFilterMenuItem()
|
||||
: base(null)
|
||||
: base("All beatmaps")
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -66,9 +66,8 @@ namespace osu.Game.Collections
|
||||
public class ManageCollectionsFilterMenuItem : CollectionFilterMenuItem
|
||||
{
|
||||
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.
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Collections
|
||||
{
|
||||
public class CollectionToggleMenuItem : ToggleMenuItem
|
||||
{
|
||||
public CollectionToggleMenuItem(BeatmapCollection collection, IBeatmapInfo beatmap)
|
||||
: base(collection.Name.Value, MenuItemType.Standard, state =>
|
||||
public CollectionToggleMenuItem(Live<BeatmapCollection> collection, IBeatmapInfo beatmap)
|
||||
: base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state =>
|
||||
{
|
||||
if (state)
|
||||
collection.BeatmapHashes.Add(beatmap.MD5Hash);
|
||||
else
|
||||
collection.BeatmapHashes.Remove(beatmap.MD5Hash);
|
||||
collection.PerformWrite(c =>
|
||||
{
|
||||
if (state)
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using Humanizer;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Collections
|
||||
{
|
||||
public class DeleteCollectionDialog : PopupDialog
|
||||
{
|
||||
public DeleteCollectionDialog(BeatmapCollection collection, Action deleteAction)
|
||||
public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction)
|
||||
{
|
||||
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;
|
||||
|
||||
|
@ -1,39 +1,66 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osuTK;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// Visualises a list of <see cref="BeatmapCollection"/>s.
|
||||
/// </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 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 }
|
||||
};
|
||||
|
||||
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 new DrawableCollectionListItem(item, true);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
realmSubscription?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The scroll container for this <see cref="DrawableCollectionList"/>.
|
||||
/// Contains the main flow of <see cref="DrawableCollectionListItem"/> and attaches a placeholder item to the end of the list.
|
||||
@ -46,7 +73,7 @@ namespace osu.Game.Collections
|
||||
/// <summary>
|
||||
/// The currently-displayed placeholder item.
|
||||
/// </summary>
|
||||
public DrawableCollectionListItem PlaceholderItem { get; private set; }
|
||||
public DrawableCollectionListItem PlaceholderItem { get; private set; } = null!;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
private readonly Container content;
|
||||
@ -76,6 +103,7 @@ namespace osu.Game.Collections
|
||||
});
|
||||
|
||||
ReplacePlaceholder();
|
||||
Debug.Assert(PlaceholderItem != null);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -95,7 +123,7 @@ namespace osu.Game.Collections
|
||||
var previous = PlaceholderItem;
|
||||
|
||||
placeholderContainer.Clear(false);
|
||||
placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection(), false));
|
||||
placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection().ToLiveUnmanaged(), false));
|
||||
|
||||
return previous;
|
||||
}
|
||||
@ -104,7 +132,7 @@ namespace osu.Game.Collections
|
||||
/// <summary>
|
||||
/// The flow of <see cref="DrawableCollectionListItem"/>. Disables layout easing unless a drag is in progress.
|
||||
/// </summary>
|
||||
private class Flow : FillFlowContainer<RearrangeableListItem<BeatmapCollection>>
|
||||
private class Flow : FillFlowContainer<RearrangeableListItem<Live<BeatmapCollection>>>
|
||||
{
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -24,79 +23,62 @@ namespace osu.Game.Collections
|
||||
/// <summary>
|
||||
/// Visualises a <see cref="BeatmapCollection"/> inside a <see cref="DrawableCollectionList"/>.
|
||||
/// </summary>
|
||||
public class DrawableCollectionListItem : OsuRearrangeableListItem<BeatmapCollection>
|
||||
public class DrawableCollectionListItem : OsuRearrangeableListItem<Live<BeatmapCollection>>
|
||||
{
|
||||
private const float item_height = 35;
|
||||
private const float button_width = item_height * 0.75f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the <see cref="BeatmapCollection"/> currently exists inside the <see cref="CollectionManager"/>.
|
||||
/// </summary>
|
||||
public IBindable<bool> IsCreated => isCreated;
|
||||
|
||||
private readonly Bindable<bool> isCreated = new Bindable<bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="DrawableCollectionListItem"/>.
|
||||
/// </summary>
|
||||
/// <param name="item">The <see cref="BeatmapCollection"/>.</param>
|
||||
/// <param name="isCreated">Whether <paramref name="item"/> currently exists inside the <see cref="CollectionManager"/>.</param>
|
||||
public DrawableCollectionListItem(BeatmapCollection item, bool isCreated)
|
||||
/// <param name="isCreated">Whether <paramref name="item"/> currently exists inside realm.</param>
|
||||
public DrawableCollectionListItem(Live<BeatmapCollection> item, bool isCreated)
|
||||
: base(item)
|
||||
{
|
||||
this.isCreated.Value = isCreated;
|
||||
|
||||
ShowDragHandle.BindTo(this.isCreated);
|
||||
ShowDragHandle.Value = item.IsManaged;
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => new ItemContent(Model)
|
||||
{
|
||||
IsCreated = { BindTarget = isCreated }
|
||||
};
|
||||
protected override Drawable CreateContent() => new ItemContent(Model);
|
||||
|
||||
/// <summary>
|
||||
/// The main content of the <see cref="DrawableCollectionListItem"/>.
|
||||
/// </summary>
|
||||
private class ItemContent : CircularContainer
|
||||
{
|
||||
public readonly Bindable<bool> IsCreated = new Bindable<bool>();
|
||||
private readonly Live<BeatmapCollection> collection;
|
||||
|
||||
private readonly IBindable<string> collectionName;
|
||||
private readonly BeatmapCollection collection;
|
||||
private ItemTextBox textBox = null!;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private CollectionManager collectionManager { get; set; }
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
private Container textBoxPaddingContainer;
|
||||
private ItemTextBox textBox;
|
||||
|
||||
public ItemContent(BeatmapCollection collection)
|
||||
public ItemContent(Live<BeatmapCollection> collection)
|
||||
{
|
||||
this.collection = collection;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = item_height;
|
||||
Masking = true;
|
||||
|
||||
collectionName = collection.Name.GetBoundCopy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
Children = new[]
|
||||
{
|
||||
new DeleteButton(collection)
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
IsCreated = { BindTarget = IsCreated },
|
||||
IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v)
|
||||
},
|
||||
textBoxPaddingContainer = new Container
|
||||
collection.IsManaged
|
||||
? new DeleteButton(collection)
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v)
|
||||
}
|
||||
: Empty(),
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Right = button_width },
|
||||
Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
textBox = new ItemTextBox
|
||||
@ -104,7 +86,7 @@ namespace osu.Game.Collections
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One,
|
||||
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();
|
||||
|
||||
// Bind late, as the collection name may change externally while still loading.
|
||||
textBox.Current = collection.Name;
|
||||
|
||||
collectionName.BindValueChanged(_ => createNewCollection(), true);
|
||||
IsCreated.BindValueChanged(created => textBoxPaddingContainer.Padding = new MarginPadding { Right = created.NewValue ? button_width : 0 }, true);
|
||||
textBox.Current.Value = collection.PerformRead(c => c.IsValid ? c.Name : string.Empty);
|
||||
textBox.OnCommit += onCommit;
|
||||
}
|
||||
|
||||
private void createNewCollection()
|
||||
private void onCommit(TextBox sender, bool newText)
|
||||
{
|
||||
if (IsCreated.Value)
|
||||
return;
|
||||
if (collection.IsManaged)
|
||||
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))
|
||||
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;
|
||||
textBox.Text = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,22 +127,17 @@ namespace osu.Game.Collections
|
||||
|
||||
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 IDialogOverlay dialogOverlay { get; set; }
|
||||
private readonly Live<BeatmapCollection> collection;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private CollectionManager collectionManager { get; set; }
|
||||
private Drawable fadeContainer = null!;
|
||||
private Drawable background = null!;
|
||||
|
||||
private readonly BeatmapCollection collection;
|
||||
|
||||
private Drawable fadeContainer;
|
||||
private Drawable background;
|
||||
|
||||
public DeleteButton(BeatmapCollection collection)
|
||||
public DeleteButton(Live<BeatmapCollection> collection)
|
||||
{
|
||||
this.collection = collection;
|
||||
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);
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
@ -227,7 +188,7 @@ namespace osu.Game.Collections
|
||||
{
|
||||
background.FlashColour(Color4.White, 150);
|
||||
|
||||
if (collection.BeatmapHashes.Count == 0)
|
||||
if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0)
|
||||
deleteCollection();
|
||||
else
|
||||
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
|
||||
@ -235,7 +196,7 @@ namespace osu.Game.Collections
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -24,10 +21,7 @@ namespace osu.Game.Collections
|
||||
private const double enter_duration = 500;
|
||||
private const double exit_duration = 200;
|
||||
|
||||
private AudioFilter lowPassFilter;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private CollectionManager collectionManager { get; set; }
|
||||
private AudioFilter lowPassFilter = null!;
|
||||
|
||||
public ManageCollectionsDialog()
|
||||
{
|
||||
@ -107,7 +101,6 @@ namespace osu.Game.Collections
|
||||
new DrawableCollectionList
|
||||
{
|
||||
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.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Settings.Sections.Maintenance;
|
||||
@ -36,15 +35,15 @@ namespace osu.Game.Database
|
||||
[Resolved]
|
||||
private ScoreManager scores { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private CollectionManager collections { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private OsuGame game { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realmAccess { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private DesktopGameHost desktopGameHost { get; set; }
|
||||
|
||||
@ -72,7 +71,7 @@ namespace osu.Game.Database
|
||||
return await new LegacySkinImporter(skins).GetAvailableCount(stableStorage);
|
||||
|
||||
case StableContent.Collections:
|
||||
return await collections.GetAvailableCount(stableStorage);
|
||||
return await new LegacyCollectionImporter(realmAccess).GetAvailableCount(stableStorage);
|
||||
|
||||
case StableContent.Scores:
|
||||
return await new LegacyScoreImporter(scores).GetAvailableCount(stableStorage);
|
||||
@ -109,7 +108,7 @@ namespace osu.Game.Database
|
||||
importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage));
|
||||
|
||||
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))
|
||||
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.Allocation;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
@ -64,8 +65,9 @@ namespace osu.Game.Database
|
||||
/// 18 2022-07-19 Added OnlineMD5Hash and LastOnlineUpdate to BeatmapInfo.
|
||||
/// 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.
|
||||
/// 21 2022-07-27 Migrate collections to realm (BeatmapCollection).
|
||||
/// </summary>
|
||||
private const int schema_version = 20;
|
||||
private const int schema_version = 21;
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
|
||||
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;
|
||||
}), rightFloatingOverlayContent.Add, true);
|
||||
|
||||
loadComponentSingleFile(new CollectionManager(Storage)
|
||||
{
|
||||
PostNotification = n => Notifications.Post(n),
|
||||
}, Add, true);
|
||||
|
||||
loadComponentSingleFile(legacyImportManager, Add);
|
||||
|
||||
loadComponentSingleFile(screenshotManager, Add);
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Music
|
||||
public Action<FilterCriteria> FilterChanged;
|
||||
|
||||
public readonly FilterTextBox Search;
|
||||
private readonly CollectionDropdown collectionDropdown;
|
||||
private readonly NowPlayingCollectionDropdown collectionDropdown;
|
||||
|
||||
public FilterControl()
|
||||
{
|
||||
@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Music
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 40,
|
||||
},
|
||||
collectionDropdown = new CollectionDropdown { RelativeSizeAxes = Axes.X }
|
||||
collectionDropdown = new NowPlayingCollectionDropdown { RelativeSizeAxes = Axes.X }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
|
||||
namespace osu.Game.Overlays.Music
|
||||
{
|
||||
@ -19,6 +20,6 @@ namespace osu.Game.Overlays.Music
|
||||
/// The collection to filter beatmaps from.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public BeatmapCollection Collection;
|
||||
public Live<BeatmapCollection> Collection;
|
||||
}
|
||||
}
|
||||
|
@ -15,9 +15,9 @@ using osu.Game.Graphics;
|
||||
namespace osu.Game.Overlays.Music
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="CollectionFilterDropdown"/> for use in the <see cref="NowPlayingOverlay"/>.
|
||||
/// A <see cref="CollectionDropdown"/> for use in the <see cref="NowPlayingOverlay"/>.
|
||||
/// </summary>
|
||||
public class CollectionDropdown : CollectionFilterDropdown
|
||||
public class NowPlayingCollectionDropdown : CollectionDropdown
|
||||
{
|
||||
protected override bool ShowManageCollectionsItem => false;
|
||||
|
@ -31,14 +31,16 @@ namespace osu.Game.Overlays.Music
|
||||
{
|
||||
var items = (SearchContainer<RearrangeableListItem<Live<BeatmapSetInfo>>>)ListContainer;
|
||||
|
||||
string[] currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray());
|
||||
|
||||
foreach (var item in items.OfType<PlaylistItem>())
|
||||
{
|
||||
if (criteria.Collection == null)
|
||||
if (currentCollectionHashes == null)
|
||||
item.InSelectedCollection = true;
|
||||
else
|
||||
{
|
||||
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.Database;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
@ -15,11 +16,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
|
||||
private SettingsButton importCollectionsButton = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(CollectionManager? collectionManager, LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay)
|
||||
{
|
||||
if (collectionManager == null) return;
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private INotificationOverlay? notificationOverlay { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay)
|
||||
{
|
||||
if (legacyImportManager?.SupportsImportFromStable == true)
|
||||
{
|
||||
Add(importCollectionsButton = new SettingsButton
|
||||
@ -38,9 +43,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
Text = MaintenanceSettingsStrings.DeleteAllCollections,
|
||||
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 FillFlowContainer mainFillFlow;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
@ -112,9 +115,6 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
[Resolved(CanBeNull = true)]
|
||||
private BeatmapSetOverlay beatmapOverlay { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private CollectionManager collectionManager { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
||||
|
||||
@ -495,11 +495,11 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
if (beatmapOverlay != null)
|
||||
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)
|
||||
{
|
||||
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)
|
||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||
|
||||
|
@ -76,7 +76,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
}
|
||||
|
||||
if (match)
|
||||
match &= criteria.Collection?.BeatmapHashes.Contains(BeatmapInfo.MD5Hash) ?? true;
|
||||
match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true;
|
||||
|
||||
if (match && criteria.RulesetCriteria != null)
|
||||
match &= criteria.RulesetCriteria.Matches(BeatmapInfo);
|
||||
|
@ -22,6 +22,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -63,12 +64,12 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private CollectionManager collectionManager { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; }
|
||||
|
||||
private IBindable<StarDifficulty?> starDifficultyBindable;
|
||||
private CancellationTokenSource starDifficultyCancellationSource;
|
||||
|
||||
@ -237,14 +238,11 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null)
|
||||
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID)));
|
||||
|
||||
if (collectionManager != null)
|
||||
{
|
||||
var collectionItems = collectionManager.Collections.Select(c => new CollectionToggleMenuItem(c, beatmapInfo)).Cast<OsuMenuItem>().ToList();
|
||||
if (manageCollectionsDialog != null)
|
||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||
var collectionItems = realm.Realm.All<BeatmapCollection>().AsEnumerable().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast<OsuMenuItem>().ToList();
|
||||
if (manageCollectionsDialog != null)
|
||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||
|
||||
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
|
||||
}
|
||||
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
|
||||
|
||||
if (hideRequested != null)
|
||||
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.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
@ -32,12 +33,12 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private CollectionManager collectionManager { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; }
|
||||
|
||||
public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty<DrawableCarouselItem>() : beatmapContainer.AliveChildren;
|
||||
|
||||
[CanBeNull]
|
||||
@ -223,14 +224,11 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
if (beatmapSet.OnlineID > 0 && viewDetails != null)
|
||||
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID)));
|
||||
|
||||
if (collectionManager != null)
|
||||
{
|
||||
var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList();
|
||||
if (manageCollectionsDialog != null)
|
||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||
var collectionItems = realm.Realm.All<BeatmapCollection>().AsEnumerable().Select(createCollectionMenuItem).ToList();
|
||||
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))
|
||||
items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet)));
|
||||
@ -247,7 +245,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
|
||||
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)
|
||||
state = TernaryState.True;
|
||||
@ -256,24 +254,29 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
else
|
||||
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:
|
||||
if (collection.BeatmapHashes.Contains(b.MD5Hash))
|
||||
continue;
|
||||
switch (s)
|
||||
{
|
||||
case TernaryState.True:
|
||||
if (c.BeatmapMD5Hashes.Contains(b.MD5Hash))
|
||||
continue;
|
||||
|
||||
collection.BeatmapHashes.Add(b.MD5Hash);
|
||||
break;
|
||||
c.BeatmapMD5Hashes.Add(b.MD5Hash);
|
||||
break;
|
||||
|
||||
case TernaryState.False:
|
||||
collection.BeatmapHashes.Remove(b.MD5Hash);
|
||||
break;
|
||||
case TernaryState.False:
|
||||
c.BeatmapMD5Hashes.Remove(b.MD5Hash);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
{
|
||||
State = { Value = state }
|
||||
|
@ -39,6 +39,10 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private Bindable<GroupMode> groupMode;
|
||||
|
||||
private SeekLimitedSearchTextBox searchTextBox;
|
||||
|
||||
private CollectionDropdown collectionDropdown;
|
||||
|
||||
public FilterCriteria CreateCriteria()
|
||||
{
|
||||
string query = searchTextBox.Text;
|
||||
@ -49,7 +53,7 @@ namespace osu.Game.Screens.Select
|
||||
Sort = sortMode.Value,
|
||||
AllowConvertedBeatmaps = showConverted.Value,
|
||||
Ruleset = ruleset.Value,
|
||||
Collection = collectionDropdown?.Current.Value?.Collection
|
||||
CollectionBeatmapMD5Hashes = collectionDropdown.Current.Value?.Collection?.PerformRead(c => c.BeatmapMD5Hashes)
|
||||
};
|
||||
|
||||
if (!minimumStars.IsDefault)
|
||||
@ -64,10 +68,6 @@ namespace osu.Game.Screens.Select
|
||||
return criteria;
|
||||
}
|
||||
|
||||
private SeekLimitedSearchTextBox searchTextBox;
|
||||
|
||||
private CollectionFilterDropdown collectionDropdown;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
base.ReceivePositionalInputAt(screenSpacePos) || sortTabs.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
@ -179,10 +179,11 @@ namespace osu.Game.Screens.Select
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.48f,
|
||||
},
|
||||
collectionDropdown = new CollectionFilterDropdown
|
||||
collectionDropdown = new CollectionDropdown
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RequestFilter = updateCriteria,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Y = 4,
|
||||
Width = 0.5f,
|
||||
@ -209,15 +210,6 @@ namespace osu.Game.Screens.Select
|
||||
groupMode.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();
|
||||
|
||||
updateCriteria();
|
||||
|
@ -68,10 +68,10 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The collection to filter beatmaps from.
|
||||
/// Hashes from the <see cref="BeatmapCollection"/> to filter to.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public BeatmapCollection Collection;
|
||||
public IEnumerable<string> CollectionBeatmapMD5Hashes { get; set; }
|
||||
|
||||
[CanBeNull]
|
||||
public IRulesetFilterCriteria RulesetCriteria { get; set; }
|
||||
|
Loading…
Reference in New Issue
Block a user