From 6b73f7c7ec5b411c9f30ec7b9c8d94d2a2d12e34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 15:04:09 +0900 Subject: [PATCH 01/17] Split out legacy import path from realm manager --- .../Collections/IO/ImportCollectionsTest.cs | 3 +- osu.Game.Tests/ImportTest.cs | 2 +- .../TestSceneManageCollectionsDialog.cs | 2 +- .../SongSelect/TestSceneFilterControl.cs | 2 +- osu.Game/Beatmaps/RealmBeatmapCollection.cs | 40 +++ osu.Game/Collections/BeatmapCollection.cs | 17 - osu.Game/Collections/CollectionManager.cs | 331 ++---------------- osu.Game/Database/LegacyCollectionImporter.cs | 167 +++++++++ osu.Game/Database/LegacyImportManager.cs | 4 +- osu.Game/OsuGame.cs | 2 +- 10 files changed, 237 insertions(+), 333 deletions(-) create mode 100644 osu.Game/Beatmaps/RealmBeatmapCollection.cs create mode 100644 osu.Game/Database/LegacyCollectionImporter.cs diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index 9a8f29647d..685586ff02 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -11,6 +11,7 @@ using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Game.Database; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Collections.IO @@ -187,7 +188,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.CollectionManager).Import(stream).WaitSafely(), TaskCreationOptions.LongRunning); } } } diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs index 32b6dc649c..1f18f92158 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests if (withBeatmap) BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely(); - AddInternal(CollectionManager = new CollectionManager(Storage)); + AddInternal(CollectionManager = new CollectionManager()); } } } diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 3f30fa367c..789139f483 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Collections base.Content.AddRange(new Drawable[] { - manager = new CollectionManager(LocalStorage), + manager = new CollectionManager(), Content, dialogOverlay = new DialogOverlay(), }); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 6807180640..01bf312ddd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.SongSelect base.Content.AddRange(new Drawable[] { - collectionManager = new CollectionManager(LocalStorage), + collectionManager = new CollectionManager(), Content }); diff --git a/osu.Game/Beatmaps/RealmBeatmapCollection.cs b/osu.Game/Beatmaps/RealmBeatmapCollection.cs new file mode 100644 index 0000000000..d3261fc39e --- /dev/null +++ b/osu.Game/Beatmaps/RealmBeatmapCollection.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using osu.Game.Database; +using Realms; + +namespace osu.Game.Beatmaps +{ + public class RealmBeatmapCollection : RealmObject, IHasGuidPrimaryKey + { + [PrimaryKey] + public Guid ID { get; } + + public string Name { get; set; } = string.Empty; + + public List BeatmapMD5Hashes { get; set; } = null!; + + /// + /// The date when this collection was last modified. + /// + public DateTimeOffset LastModified { get; set; } + + public RealmBeatmapCollection(string? name, List? beatmapMD5Hashes) + { + ID = Guid.NewGuid(); + Name = name ?? string.Empty; + BeatmapMD5Hashes = beatmapMD5Hashes ?? new List(); + + LastModified = DateTimeOffset.UtcNow; + } + + [UsedImplicitly] + private RealmBeatmapCollection() + { + } + } +} diff --git a/osu.Game/Collections/BeatmapCollection.cs b/osu.Game/Collections/BeatmapCollection.cs index 742d757bec..abfd0e6dd0 100644 --- a/osu.Game/Collections/BeatmapCollection.cs +++ b/osu.Game/Collections/BeatmapCollection.cs @@ -14,11 +14,6 @@ namespace osu.Game.Collections /// public class BeatmapCollection { - /// - /// Invoked whenever any change occurs on this . - /// - public event Action Changed; - /// /// The collection's name. /// @@ -33,17 +28,5 @@ namespace osu.Game.Collections /// The date when this collection was last modified. /// public DateTimeOffset LastModifyDate { get; private set; } = DateTimeOffset.UtcNow; - - public BeatmapCollection() - { - BeatmapHashes.CollectionChanged += (_, _) => onChange(); - Name.ValueChanged += _ => onChange(); - } - - private void onChange() - { - LastModifyDate = DateTimeOffset.Now; - Changed?.Invoke(); - } } } diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 796b3c426c..0d4ee5c722 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -4,346 +4,59 @@ #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.Beatmaps; using osu.Game.Database; -using osu.Game.IO; -using osu.Game.IO.Legacy; using osu.Game.Overlays.Notifications; +using Realms; namespace osu.Game.Collections { /// /// Handles user-defined collections of beatmaps. /// - /// - /// 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. - /// public class CollectionManager : Component, IPostNotifications { - /// - /// Database version in stable-compatible YYYYMMDD format. - /// - 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 Collections = new BindableList(); - private readonly Storage storage; - - public CollectionManager(Storage storage) - { - this.storage = storage; - } - - [Resolved(canBeNull: true)] - private DatabaseContextFactory efContextFactory { get; set; } = null!; + [Resolved] + private RealmAccess realm { get; set; } [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 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(() => + protected override void LoadComplete() { - switch (e.Action) + base.LoadComplete(); + + realm.RegisterForNotifications(r => r.All(), collectionsChanged); + } + + private void collectionsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + { + // TODO: hook up with realm changes. + + if (changes == null) { - case NotifyCollectionChangedAction.Add: - foreach (var c in e.NewItems.Cast()) - c.Changed += backgroundSave; - break; - - case NotifyCollectionChangedAction.Remove: - foreach (var c in e.OldItems.Cast()) - c.Changed -= backgroundSave; - break; - - case NotifyCollectionChangedAction.Replace: - foreach (var c in e.OldItems.Cast()) - c.Changed -= backgroundSave; - - foreach (var c in e.NewItems.Cast()) - c.Changed += backgroundSave; - break; + foreach (var collection in sender) + Collections.Add(new BeatmapCollection + { + Name = { Value = collection.Name }, + BeatmapHashes = { Value = collection.BeatmapMD5Hashes }, + }); } - - backgroundSave(); - }); + } public Action PostNotification { protected get; set; } - public Task 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; - }); - } - - /// - /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. - /// - 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 newCollections) - { - var tcs = new TaskCompletionSource(); - - 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 readCollections(Stream stream, ProgressNotification notification = null) - { - if (notification != null) - { - notification.Text = "Reading collections..."; - notification.Progress = 0; - } - - var result = new List(); - - 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; - - /// - /// Perform a save with debounce. - /// - 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(); - } } } diff --git a/osu.Game/Database/LegacyCollectionImporter.cs b/osu.Game/Database/LegacyCollectionImporter.cs new file mode 100644 index 0000000000..8168419e80 --- /dev/null +++ b/osu.Game/Database/LegacyCollectionImporter.cs @@ -0,0 +1,167 @@ +// Copyright (c) ppy Pty Ltd . 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.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Logging; +using osu.Game.Collections; +using osu.Game.IO; +using osu.Game.IO.Legacy; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Database +{ + public class LegacyCollectionImporter + { + private readonly CollectionManager collections; + + public LegacyCollectionImporter(CollectionManager collections) + { + this.collections = collections; + } + + public Action PostNotification { protected get; set; } + + private const string database_name = "collection.db"; + + public Task 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; + }); + } + + /// + /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. + /// + 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 importedCollections = readCollections(stream, notification); + await importCollections(importedCollections).ConfigureAwait(false); + + notification.CompletionText = $"Imported {importedCollections.Count} collections"; + notification.State = ProgressNotificationState.Completed; + } + + private Task importCollections(List newCollections) + { + var tcs = new TaskCompletionSource(); + + // Schedule(() => + // { + try + { + foreach (var newCol in newCollections) + { + var existing = collections.Collections.FirstOrDefault(c => c.Name.Value == newCol.Name.Value); + if (existing == null) + collections.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 readCollections(Stream stream, ProgressNotification notification = null) + { + if (notification != null) + { + notification.Text = "Reading collections..."; + notification.Progress = 0; + } + + var result = new List(); + + 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; + } + } +} diff --git a/osu.Game/Database/LegacyImportManager.cs b/osu.Game/Database/LegacyImportManager.cs index f40e0d33c2..05bd5ceb54 100644 --- a/osu.Game/Database/LegacyImportManager.cs +++ b/osu.Game/Database/LegacyImportManager.cs @@ -72,7 +72,7 @@ namespace osu.Game.Database return await new LegacySkinImporter(skins).GetAvailableCount(stableStorage); case StableContent.Collections: - return await collections.GetAvailableCount(stableStorage); + return await new LegacyCollectionImporter(collections).GetAvailableCount(stableStorage); case StableContent.Scores: return await new LegacyScoreImporter(scores).GetAvailableCount(stableStorage); @@ -109,7 +109,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(collections).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); if (content.HasFlagFast(StableContent.Scores)) importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1ee53e2848..8d8864a46a 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -858,7 +858,7 @@ namespace osu.Game d.Origin = Anchor.TopRight; }), rightFloatingOverlayContent.Add, true); - loadComponentSingleFile(new CollectionManager(Storage) + loadComponentSingleFile(new CollectionManager { PostNotification = n => Notifications.Post(n), }, Add, true); From 9c543fef481b0afda90d60288a111dd10a544067 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 15:59:36 +0900 Subject: [PATCH 02/17] Remove `CollectionManager` --- .../Collections/IO/ImportCollectionsTest.cs | 83 ++++++++---- osu.Game.Tests/ImportTest.cs | 6 +- .../TestSceneManageCollectionsDialog.cs | 120 +++++++++++------- .../SongSelect/TestSceneFilterControl.cs | 52 ++++---- osu.Game/Beatmaps/RealmBeatmapCollection.cs | 4 +- .../Collections/CollectionFilterDropdown.cs | 6 +- osu.Game/Collections/CollectionManager.cs | 62 --------- .../Collections/CollectionToggleMenuItem.cs | 10 +- .../Collections/DeleteCollectionDialog.cs | 5 +- .../Collections/DrawableCollectionList.cs | 17 ++- .../Collections/DrawableCollectionListItem.cs | 47 ++++--- .../Collections/ManageCollectionsDialog.cs | 5 - osu.Game/Database/LegacyCollectionImporter.cs | 54 ++++---- osu.Game/Database/LegacyImportManager.cs | 11 +- osu.Game/OsuGame.cs | 5 - .../Maintenance/CollectionsSettings.cs | 23 +++- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 10 +- .../Carousel/DrawableCarouselBeatmap.cs | 18 ++- .../Carousel/DrawableCarouselBeatmapSet.cs | 30 ++--- 19 files changed, 276 insertions(+), 292 deletions(-) delete mode 100644 osu.Game/Collections/CollectionManager.cs diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index 685586ff02..32503cdb12 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -5,12 +5,14 @@ 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.Beatmaps; using osu.Game.Database; using osu.Game.Tests.Resources; @@ -30,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().ToList(); + Assert.That(collections.Count, Is.Zero); + }); } finally { @@ -50,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().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 { @@ -81,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().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 { @@ -124,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().ToList(); + Assert.That(collections.Count, Is.EqualTo(0)); + }); } finally { @@ -149,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().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 { @@ -169,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().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 { @@ -188,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(() => new LegacyCollectionImporter(osu.CollectionManager).Import(stream).WaitSafely(), TaskCreationOptions.LongRunning); + await Task.Factory.StartNew(() => new LegacyCollectionImporter(osu.Realm).Import(stream).WaitSafely(), TaskCreationOptions.LongRunning); } } } diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs index 1f18f92158..23ca31ee42 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.cs @@ -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(); 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()); } } } diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 789139f483..21d2b0328b 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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,13 +25,10 @@ namespace osu.Game.Tests.Visual.Collections { protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; - private DialogOverlay dialogOverlay; - private CollectionManager manager; - - private RulesetStore rulesets; - private BeatmapManager beatmapManager; - - private ManageCollectionsDialog dialog; + private DialogOverlay dialogOverlay = null!; + private RulesetStore rulesets = null!; + private BeatmapManager beatmapManager = null!; + private ManageCollectionsDialog dialog = null!; [BackgroundDependencyLoader] private void load(GameHost host) @@ -46,19 +41,17 @@ namespace osu.Game.Tests.Visual.Collections base.Content.AddRange(new Drawable[] { - manager = new CollectionManager(), Content, dialogOverlay = new DialogOverlay(), }); - Dependencies.Cache(manager); Dependencies.CacheAs(dialogOverlay); } [SetUp] public void SetUp() => Schedule(() => { - manager.Collections.Clear(); + Realm.Write(r => r.RemoveAll()); Child = dialog = new ManageCollectionsDialog(); }); @@ -78,17 +71,17 @@ namespace osu.Game.Tests.Visual.Collections [Test] public void TestLastItemIsPlaceholder() { - AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType().Last().Model)); + AddAssert("last item is placeholder", () => !dialog.ChildrenOfType().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 RealmBeatmapCollection(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 RealmBeatmapCollection(name: "Second collection")))); assertCollectionCount(2); assertCollectionName(1, "Second collection"); } @@ -108,7 +101,7 @@ namespace osu.Game.Tests.Visual.Collections [Test] public void TestAddCollectionViaPlaceholder() { - DrawableCollectionListItem placeholderItem = null; + DrawableCollectionListItem placeholderItem = null!; AddStep("focus placeholder", () => { @@ -117,23 +110,31 @@ namespace osu.Game.Tests.Visual.Collections }); // Done directly via the collection since InputManager methods cannot add text to textbox... - AddStep("change collection name", () => placeholderItem.Model.Name.Value = "a"); + AddStep("change collection name", () => placeholderItem.Model.Name = "a"); assertCollectionCount(1); - AddAssert("collection now exists", () => manager.Collections.Contains(placeholderItem.Model)); + AddAssert("collection now exists", () => placeholderItem.Model.IsManaged); - AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType().Last().Model)); + AddAssert("last item is placeholder", () => !dialog.ChildrenOfType().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" } }, - })); + RealmBeatmapCollection first = null!; - AddStep("remove first collection", () => manager.Collections.RemoveAt(0)); + AddStep("add two collections", () => + { + Realm.Write(r => + { + r.Add(new[] + { + first = new RealmBeatmapCollection(name: "1"), + new RealmBeatmapCollection(name: "2"), + }); + }); + }); + + AddStep("change first collection name", () => Realm.Write(r => r.Remove(first))); assertCollectionCount(1); assertCollectionName(0, "2"); } @@ -151,21 +152,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 RealmBeatmapCollection(name: "1"), + new RealmBeatmapCollection(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 RealmBeatmapCollection(name: "1"), + new RealmBeatmapCollection(name: "2") + { + BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } + }, + }))); assertCollectionCount(2); @@ -198,10 +205,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 RealmBeatmapCollection(name: "1") + { + BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } + }, + }))); assertCollectionCount(1); @@ -224,13 +234,21 @@ namespace osu.Game.Tests.Visual.Collections [Test] public void TestCollectionRenamedExternal() { - AddStep("add two collections", () => manager.Collections.AddRange(new[] - { - new BeatmapCollection { Name = { Value = "1" } }, - new BeatmapCollection { Name = { Value = "2" } }, - })); + RealmBeatmapCollection first = null!; - AddStep("change first collection name", () => manager.Collections[0].Name.Value = "First"); + AddStep("add two collections", () => + { + Realm.Write(r => + { + r.Add(new[] + { + first = new RealmBeatmapCollection(name: "1"), + new RealmBeatmapCollection(name: "2"), + }); + }); + }); + + AddStep("change first collection name", () => Realm.Write(_ => first.Name = "First")); assertCollectionName(0, "First"); } @@ -238,16 +256,24 @@ namespace osu.Game.Tests.Visual.Collections [Test] public void TestCollectionRenamedOnTextChange() { - AddStep("add two collections", () => manager.Collections.AddRange(new[] + RealmBeatmapCollection first = null!; + + AddStep("add two collections", () => { - new BeatmapCollection { Name = { Value = "1" } }, - new BeatmapCollection { Name = { Value = "2" } }, - })); + Realm.Write(r => + { + r.Add(new[] + { + first = new RealmBeatmapCollection(name: "1"), + new RealmBeatmapCollection(name: "2"), + }); + }); + }); assertCollectionCount(2); AddStep("change first collection name", () => dialog.ChildrenOfType().First().Text = "First"); - AddAssert("collection has new name", () => manager.Collections[0].Name.Value == "First"); + AddUntilStep("collection has new name", () => first.Name == "First"); } private void assertCollectionCount(int count) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 01bf312ddd..e4d69334a3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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,12 +26,9 @@ namespace osu.Game.Tests.Visual.SongSelect { protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; - private CollectionManager collectionManager; - - private RulesetStore rulesets; - private BeatmapManager beatmapManager; - - private FilterControl control; + private RulesetStore rulesets = null!; + private BeatmapManager beatmapManager = null!; + private FilterControl control = null!; [BackgroundDependencyLoader] private void load(GameHost host) @@ -46,17 +41,14 @@ namespace osu.Game.Tests.Visual.SongSelect base.Content.AddRange(new Drawable[] { - collectionManager = new CollectionManager(), Content }); - - Dependencies.Cache(collectionManager); } [SetUp] public void SetUp() => Schedule(() => { - collectionManager.Collections.Clear(); + Realm.Write(r => r.RemoveAll()); Child = control = new FilterControl { @@ -77,8 +69,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 RealmBeatmapCollection(name: "1")))); + AddStep("add collection", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "2")))); assertCollectionDropdownContains("1"); assertCollectionDropdownContains("2"); } @@ -86,9 +78,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)); + var first = new RealmBeatmapCollection(name: "1"); + + AddStep("add collection", () => Realm.Write(r => r.Add(first))); + AddStep("add collection", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "2")))); + AddStep("remove collection", () => Realm.Write(r => r.Remove(first))); assertCollectionDropdownContains("1", false); assertCollectionDropdownContains("2"); @@ -97,7 +91,7 @@ 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 RealmBeatmapCollection(name: "1")))); AddStep("select collection", () => { var dropdown = control.ChildrenOfType().Single(); @@ -106,7 +100,7 @@ namespace osu.Game.Tests.Visual.SongSelect addExpandHeaderStep(); - AddStep("change name", () => collectionManager.Collections[0].Name.Value = "First"); + AddStep("change name", () => Realm.Write(_ => getFirstCollection().Name = "First")); assertCollectionDropdownContains("First"); assertCollectionHeaderDisplays("First"); @@ -124,7 +118,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 RealmBeatmapCollection(name: "1")))); AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1))); AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent); } @@ -134,7 +128,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 RealmBeatmapCollection(name: "1")))); AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value); @@ -150,13 +144,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 RealmBeatmapCollection(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", () => 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", () => getFirstCollection().BeatmapMD5Hashes.Clear()); AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); } @@ -167,15 +161,15 @@ 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 RealmBeatmapCollection(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)); } @@ -184,7 +178,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 RealmBeatmapCollection(name: "1")))); AddStep("select collection", () => { InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1)); @@ -202,6 +196,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("collection filter still selected", () => control.CreateCriteria().Collection?.Name.Value == "1"); } + private RealmBeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); + private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) => AddAssert($"collection dropdown header displays '{collectionName}'", () => shouldDisplay == (control.ChildrenOfType().Single().ChildrenOfType().First().Text == collectionName)); diff --git a/osu.Game/Beatmaps/RealmBeatmapCollection.cs b/osu.Game/Beatmaps/RealmBeatmapCollection.cs index d3261fc39e..22ba9d5789 100644 --- a/osu.Game/Beatmaps/RealmBeatmapCollection.cs +++ b/osu.Game/Beatmaps/RealmBeatmapCollection.cs @@ -16,14 +16,14 @@ namespace osu.Game.Beatmaps public string Name { get; set; } = string.Empty; - public List BeatmapMD5Hashes { get; set; } = null!; + public IList BeatmapMD5Hashes { get; } = null!; /// /// The date when this collection was last modified. /// public DateTimeOffset LastModified { get; set; } - public RealmBeatmapCollection(string? name, List? beatmapMD5Hashes) + public RealmBeatmapCollection(string? name = null, IList? beatmapMD5Hashes = null) { ID = Guid.NewGuid(); Name = name ?? string.Empty; diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index d099eb6e1b..ed2c0c7cfb 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -46,9 +46,6 @@ namespace osu.Game.Collections [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } - [Resolved(CanBeNull = true)] - private CollectionManager collectionManager { get; set; } - public CollectionFilterDropdown() { ItemSource = filters; @@ -59,8 +56,7 @@ namespace osu.Game.Collections { base.LoadComplete(); - if (collectionManager != null) - collections.BindTo(collectionManager.Collections); + // TODO: bind to realm data // 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. diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs deleted file mode 100644 index 0d4ee5c722..0000000000 --- a/osu.Game/Collections/CollectionManager.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) ppy Pty Ltd . 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.Graphics; -using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Overlays.Notifications; -using Realms; - -namespace osu.Game.Collections -{ - /// - /// Handles user-defined collections of beatmaps. - /// - public class CollectionManager : Component, IPostNotifications - { - public readonly BindableList Collections = new BindableList(); - - [Resolved] - private RealmAccess realm { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - realm.RegisterForNotifications(r => r.All(), collectionsChanged); - } - - private void collectionsChanged(IRealmCollection sender, ChangeSet changes, Exception error) - { - // TODO: hook up with realm changes. - - if (changes == null) - { - foreach (var collection in sender) - Collections.Add(new BeatmapCollection - { - Name = { Value = collection.Name }, - BeatmapHashes = { Value = collection.BeatmapMD5Hashes }, - }); - } - } - - public Action PostNotification { protected get; set; } - - public void DeleteAll() - { - Collections.Clear(); - PostNotification?.Invoke(new ProgressCompletionNotification { Text = "Deleted all collections!" }); - } - } -} diff --git a/osu.Game/Collections/CollectionToggleMenuItem.cs b/osu.Game/Collections/CollectionToggleMenuItem.cs index f2b10305b8..632249913d 100644 --- a/osu.Game/Collections/CollectionToggleMenuItem.cs +++ b/osu.Game/Collections/CollectionToggleMenuItem.cs @@ -8,16 +8,16 @@ namespace osu.Game.Collections { public class CollectionToggleMenuItem : ToggleMenuItem { - public CollectionToggleMenuItem(BeatmapCollection collection, IBeatmapInfo beatmap) - : base(collection.Name.Value, MenuItemType.Standard, state => + public CollectionToggleMenuItem(RealmBeatmapCollection collection, IBeatmapInfo beatmap) + : base(collection.Name, MenuItemType.Standard, state => { if (state) - collection.BeatmapHashes.Add(beatmap.MD5Hash); + collection.BeatmapMD5Hashes.Add(beatmap.MD5Hash); else - collection.BeatmapHashes.Remove(beatmap.MD5Hash); + collection.BeatmapMD5Hashes.Remove(beatmap.MD5Hash); }) { - State.Value = collection.BeatmapHashes.Contains(beatmap.MD5Hash); + State.Value = collection.BeatmapMD5Hashes.Contains(beatmap.MD5Hash); } } } diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs index 1da2870913..33c2174623 100644 --- a/osu.Game/Collections/DeleteCollectionDialog.cs +++ b/osu.Game/Collections/DeleteCollectionDialog.cs @@ -6,16 +6,17 @@ using System; using Humanizer; using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; using osu.Game.Overlays.Dialog; namespace osu.Game.Collections { public class DeleteCollectionDialog : PopupDialog { - public DeleteCollectionDialog(BeatmapCollection collection, Action deleteAction) + public DeleteCollectionDialog(RealmBeatmapCollection collection, Action deleteAction) { HeaderText = "Confirm deletion of"; - BodyText = $"{collection.Name.Value} ({"beatmap".ToQuantity(collection.BeatmapHashes.Count)})"; + BodyText = $"{collection.Name} ({"beatmap".ToQuantity(collection.BeatmapMD5Hashes.Count)})"; Icon = FontAwesome.Regular.TrashAlt; diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 4fe5733c2f..63f04641f4 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -7,28 +7,31 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osuTK; namespace osu.Game.Collections { /// - /// Visualises a list of s. + /// Visualises a list of s. /// - public class DrawableCollectionList : OsuRearrangeableListContainer + public class DrawableCollectionList : OsuRearrangeableListContainer { private Scroll scroll; protected override ScrollContainer CreateScrollContainer() => scroll = new Scroll(); - protected override FillFlowContainer> CreateListFillFlowContainer() => new Flow + protected override FillFlowContainer> CreateListFillFlowContainer() => new Flow { DragActive = { BindTarget = DragActive } }; - protected override OsuRearrangeableListItem CreateOsuDrawable(BeatmapCollection item) + // TODO: source from realm + + protected override OsuRearrangeableListItem CreateOsuDrawable(RealmBeatmapCollection item) { - if (item == scroll.PlaceholderItem.Model) + if (item.ID == scroll.PlaceholderItem.Model.ID) return scroll.ReplacePlaceholder(); return new DrawableCollectionListItem(item, true); @@ -95,7 +98,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 RealmBeatmapCollection(), false)); return previous; } @@ -104,7 +107,7 @@ namespace osu.Game.Collections /// /// The flow of . Disables layout easing unless a drag is in progress. /// - private class Flow : FillFlowContainer> + private class Flow : FillFlowContainer> { public readonly IBindable DragActive = new Bindable(); diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 4596fc0e52..a29b2ef81c 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -12,6 +12,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -22,15 +24,15 @@ using osuTK.Graphics; namespace osu.Game.Collections { /// - /// Visualises a inside a . + /// Visualises a inside a . /// - public class DrawableCollectionListItem : OsuRearrangeableListItem + public class DrawableCollectionListItem : OsuRearrangeableListItem { private const float item_height = 35; private const float button_width = item_height * 0.75f; /// - /// Whether the currently exists inside the . + /// Whether the currently exists inside realm. /// public IBindable IsCreated => isCreated; @@ -39,9 +41,9 @@ namespace osu.Game.Collections /// /// Creates a new . /// - /// The . - /// Whether currently exists inside the . - public DrawableCollectionListItem(BeatmapCollection item, bool isCreated) + /// The . + /// Whether currently exists inside realm. + public DrawableCollectionListItem(RealmBeatmapCollection item, bool isCreated) : base(item) { this.isCreated.Value = isCreated; @@ -61,24 +63,18 @@ namespace osu.Game.Collections { public readonly Bindable IsCreated = new Bindable(); - private readonly IBindable collectionName; - private readonly BeatmapCollection collection; - - [Resolved(CanBeNull = true)] - private CollectionManager collectionManager { get; set; } + private readonly RealmBeatmapCollection collection; private Container textBoxPaddingContainer; private ItemTextBox textBox; - public ItemContent(BeatmapCollection collection) + public ItemContent(RealmBeatmapCollection collection) { this.collection = collection; RelativeSizeAxes = Axes.X; Height = item_height; Masking = true; - - collectionName = collection.Name.GetBoundCopy(); } [BackgroundDependencyLoader] @@ -111,14 +107,17 @@ namespace osu.Game.Collections }; } + [Resolved] + private RealmAccess realm { get; set; } + protected override void LoadComplete() { base.LoadComplete(); // Bind late, as the collection name may change externally while still loading. - textBox.Current = collection.Name; + textBox.Current.Value = collection.Name; + textBox.Current.BindValueChanged(_ => createNewCollection(), true); - collectionName.BindValueChanged(_ => createNewCollection(), true); IsCreated.BindValueChanged(created => textBoxPaddingContainer.Padding = new MarginPadding { Right = created.NewValue ? button_width : 0 }, true); } @@ -127,11 +126,11 @@ namespace osu.Game.Collections if (IsCreated.Value) return; - if (string.IsNullOrEmpty(collectionName.Value)) + if (string.IsNullOrEmpty(textBox.Current.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); + realm.Write(r => r.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. @@ -162,15 +161,15 @@ namespace osu.Game.Collections [Resolved(CanBeNull = true)] private IDialogOverlay dialogOverlay { get; set; } - [Resolved(CanBeNull = true)] - private CollectionManager collectionManager { get; set; } + [Resolved] + private RealmAccess realmAccess { get; set; } - private readonly BeatmapCollection collection; + private readonly RealmBeatmapCollection collection; private Drawable fadeContainer; private Drawable background; - public DeleteButton(BeatmapCollection collection) + public DeleteButton(RealmBeatmapCollection collection) { this.collection = collection; RelativeSizeAxes = Axes.Y; @@ -227,7 +226,7 @@ namespace osu.Game.Collections { background.FlashColour(Color4.White, 150); - if (collection.BeatmapHashes.Count == 0) + if (collection.BeatmapMD5Hashes.Count == 0) deleteCollection(); else dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection)); @@ -235,7 +234,7 @@ namespace osu.Game.Collections return true; } - private void deleteCollection() => collectionManager?.Collections.Remove(collection); + private void deleteCollection() => realmAccess.Write(r => r.Remove(collection)); } } } diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index a9d699bc9f..721e0d632e 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -5,7 +5,6 @@ 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; @@ -26,9 +25,6 @@ namespace osu.Game.Collections private AudioFilter lowPassFilter; - [Resolved(CanBeNull = true)] - private CollectionManager collectionManager { get; set; } - public ManageCollectionsDialog() { Anchor = Anchor.Centre; @@ -107,7 +103,6 @@ namespace osu.Game.Collections new DrawableCollectionList { RelativeSizeAxes = Axes.Both, - Items = { BindTarget = collectionManager?.Collections ?? new BindableList() } } } } diff --git a/osu.Game/Database/LegacyCollectionImporter.cs b/osu.Game/Database/LegacyCollectionImporter.cs index 8168419e80..aa98c491b1 100644 --- a/osu.Game/Database/LegacyCollectionImporter.cs +++ b/osu.Game/Database/LegacyCollectionImporter.cs @@ -1,14 +1,13 @@ // Copyright (c) ppy Pty Ltd . 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.IO; using System.Linq; using System.Threading.Tasks; using osu.Framework.Logging; -using osu.Game.Collections; +using osu.Game.Beatmaps; using osu.Game.IO; using osu.Game.IO.Legacy; using osu.Game.Overlays.Notifications; @@ -17,17 +16,17 @@ namespace osu.Game.Database { public class LegacyCollectionImporter { - private readonly CollectionManager collections; + public Action? PostNotification { protected get; set; } - public LegacyCollectionImporter(CollectionManager collections) - { - this.collections = collections; - } - - public Action PostNotification { protected get; set; } + private readonly RealmAccess realm; private const string database_name = "collection.db"; + public LegacyCollectionImporter(RealmAccess realm) + { + this.realm = realm; + } + public Task GetAvailableCount(StableStorage stableStorage) { if (!stableStorage.Exists(database_name)) @@ -76,26 +75,30 @@ namespace osu.Game.Database notification.State = ProgressNotificationState.Completed; } - private Task importCollections(List newCollections) + private Task importCollections(List newCollections) { var tcs = new TaskCompletionSource(); - // Schedule(() => - // { try { - foreach (var newCol in newCollections) + realm.Write(r => { - var existing = collections.Collections.FirstOrDefault(c => c.Name.Value == newCol.Name.Value); - if (existing == null) - collections.Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } }); - - foreach (string newBeatmap in newCol.BeatmapHashes) + foreach (var collection in newCollections) { - if (!existing.BeatmapHashes.Contains(newBeatmap)) - existing.BeatmapHashes.Add(newBeatmap); + var existing = r.All().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); } @@ -104,12 +107,11 @@ namespace osu.Game.Database Logger.Error(e, "Failed to import collection."); tcs.SetException(e); } - // }); return tcs.Task; } - private List readCollections(Stream stream, ProgressNotification notification = null) + private List readCollections(Stream stream, ProgressNotification? notification = null) { if (notification != null) { @@ -117,7 +119,7 @@ namespace osu.Game.Database notification.Progress = 0; } - var result = new List(); + var result = new List(); try { @@ -133,7 +135,7 @@ namespace osu.Game.Database if (notification?.CancellationToken.IsCancellationRequested == true) return result; - var collection = new BeatmapCollection { Name = { Value = sr.ReadString() } }; + var collection = new RealmBeatmapCollection(sr.ReadString()); int mapCount = sr.ReadInt32(); for (int j = 0; j < mapCount; j++) @@ -143,7 +145,7 @@ namespace osu.Game.Database string checksum = sr.ReadString(); - collection.BeatmapHashes.Add(checksum); + collection.BeatmapMD5Hashes.Add(checksum); } if (notification != null) diff --git a/osu.Game/Database/LegacyImportManager.cs b/osu.Game/Database/LegacyImportManager.cs index 05bd5ceb54..baa117fe07 100644 --- a/osu.Game/Database/LegacyImportManager.cs +++ b/osu.Game/Database/LegacyImportManager.cs @@ -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 new LegacyCollectionImporter(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(_ => new LegacyCollectionImporter(collections).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); + importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); if (content.HasFlagFast(StableContent.Scores)) importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8d8864a46a..78cc4d7f70 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -858,11 +858,6 @@ namespace osu.Game d.Origin = Anchor.TopRight; }), rightFloatingOverlayContent.Add, true); - loadComponentSingleFile(new CollectionManager - { - PostNotification = n => Notifications.Post(n), - }, Add, true); - loadComponentSingleFile(legacyImportManager, Add); loadComponentSingleFile(screenshotManager, Add); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs index 5367f644ca..0b17ab9c6c 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs @@ -3,9 +3,10 @@ using osu.Framework.Allocation; using osu.Framework.Localisation; -using osu.Game.Collections; +using osu.Game.Beatmaps; 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; } = null!; + + [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()); + notificationOverlay.Post(new ProgressCompletionNotification { Text = "Deleted all collections!" }); + } } } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index f38077a9a7..b17c4934cd 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -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().ToList(); + var collectionItems = realm.Realm.All().Select(c => new CollectionToggleMenuItem(c, beatmap)).Cast().ToList(); if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 50e30c68d5..bfc93b34e2 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -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 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().ToList(); - if (manageCollectionsDialog != null) - collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); + var collectionItems = realm.Realm.All().Select(c => new CollectionToggleMenuItem(c, beatmapInfo)).Cast().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))); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 8c266c8dff..3726d955bd 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -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 DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty() : 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().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))); @@ -241,13 +239,13 @@ namespace osu.Game.Screens.Select.Carousel } } - private MenuItem createCollectionMenuItem(BeatmapCollection collection) + private MenuItem createCollectionMenuItem(RealmBeatmapCollection collection) { Debug.Assert(beatmapSet != null); 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,21 +254,21 @@ namespace osu.Game.Screens.Select.Carousel else state = TernaryState.False; - return new TernaryStateToggleMenuItem(collection.Name.Value, MenuItemType.Standard, s => + return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s => { foreach (var b in beatmapSet.Beatmaps) { switch (s) { case TernaryState.True: - if (collection.BeatmapHashes.Contains(b.MD5Hash)) + if (collection.BeatmapMD5Hashes.Contains(b.MD5Hash)) continue; - collection.BeatmapHashes.Add(b.MD5Hash); + collection.BeatmapMD5Hashes.Add(b.MD5Hash); break; case TernaryState.False: - collection.BeatmapHashes.Remove(b.MD5Hash); + collection.BeatmapMD5Hashes.Remove(b.MD5Hash); break; } } From 41393616d80440b78000abd8db504e8b73f19ef1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 16:46:23 +0900 Subject: [PATCH 03/17] Replace `BeatmapCollection` with `RealmBeatmapCollection` --- .../Collections/IO/ImportCollectionsTest.cs | 14 ++-- .../TestSceneManageCollectionsDialog.cs | 38 +++++----- .../SongSelect/TestSceneFilterControl.cs | 26 +++---- osu.Game/Beatmaps/RealmBeatmapCollection.cs | 40 ----------- osu.Game/Collections/BeatmapCollection.cs | 32 +++++++-- .../Collections/CollectionFilterDropdown.cs | 70 +++++++++++-------- .../Collections/CollectionFilterMenuItem.cs | 17 ++--- .../Collections/CollectionToggleMenuItem.cs | 2 +- .../Collections/DeleteCollectionDialog.cs | 5 +- .../Collections/DrawableCollectionList.cs | 21 +++--- .../Collections/DrawableCollectionListItem.cs | 45 ++++++------ osu.Game/Database/LegacyCollectionImporter.cs | 12 ++-- osu.Game/Overlays/Music/Playlist.cs | 2 +- .../Maintenance/CollectionsSettings.cs | 4 +- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- .../Select/Carousel/CarouselBeatmap.cs | 2 +- .../Carousel/DrawableCarouselBeatmap.cs | 2 +- .../Carousel/DrawableCarouselBeatmapSet.cs | 4 +- 18 files changed, 158 insertions(+), 180 deletions(-) delete mode 100644 osu.Game/Beatmaps/RealmBeatmapCollection.cs diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index 32503cdb12..604b87dc4c 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -12,7 +12,7 @@ using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Testing; -using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Tests.Resources; @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Collections.IO osu.Realm.Run(realm => { - var collections = realm.All().ToList(); + var collections = realm.All().ToList(); Assert.That(collections.Count, Is.Zero); }); } @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Collections.IO osu.Realm.Run(realm => { - var collections = realm.All().ToList(); + var collections = realm.All().ToList(); Assert.That(collections.Count, Is.EqualTo(2)); // Even with no beatmaps imported, collections are tracking the hashes and will continue to. @@ -93,7 +93,7 @@ namespace osu.Game.Tests.Collections.IO osu.Realm.Run(realm => { - var collections = realm.All().ToList(); + var collections = realm.All().ToList(); Assert.That(collections.Count, Is.EqualTo(2)); @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Collections.IO Assert.That(exceptionThrown, Is.False); osu.Realm.Run(realm => { - var collections = realm.All().ToList(); + var collections = realm.All().ToList(); Assert.That(collections.Count, Is.EqualTo(0)); }); } @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Collections.IO // ReSharper disable once MethodHasAsyncOverload osu.Realm.Write(realm => { - var collections = realm.All().ToList(); + var collections = realm.All().ToList(); // Move first beatmap from second collection into the first. collections[0].BeatmapMD5Hashes.Add(collections[1].BeatmapMD5Hashes[0]); @@ -196,7 +196,7 @@ namespace osu.Game.Tests.Collections.IO osu.Realm.Run(realm => { - var collections = realm.All().ToList(); + var collections = realm.All().ToList(); Assert.That(collections.Count, Is.EqualTo(2)); Assert.That(collections[0].Name, Is.EqualTo("First")); diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 21d2b0328b..8de38eb4e7 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Collections [SetUp] public void SetUp() => Schedule(() => { - Realm.Write(r => r.RemoveAll()); + Realm.Write(r => r.RemoveAll()); Child = dialog = new ManageCollectionsDialog(); }); @@ -77,11 +77,11 @@ namespace osu.Game.Tests.Visual.Collections [Test] public void TestAddCollectionExternal() { - AddStep("add collection", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "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", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "Second collection")))); + AddStep("add another collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "Second collection")))); assertCollectionCount(2); assertCollectionName(1, "Second collection"); } @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Collections }); // Done directly via the collection since InputManager methods cannot add text to textbox... - AddStep("change collection name", () => placeholderItem.Model.Name = "a"); + AddStep("change collection name", () => placeholderItem.Model.PerformWrite(c => c.Name = "a")); assertCollectionCount(1); AddAssert("collection now exists", () => placeholderItem.Model.IsManaged); @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Collections [Test] public void TestRemoveCollectionExternal() { - RealmBeatmapCollection first = null!; + BeatmapCollection first = null!; AddStep("add two collections", () => { @@ -128,13 +128,13 @@ namespace osu.Game.Tests.Visual.Collections { r.Add(new[] { - first = new RealmBeatmapCollection(name: "1"), - new RealmBeatmapCollection(name: "2"), + first = new BeatmapCollection(name: "1"), + new BeatmapCollection(name: "2"), }); }); }); - AddStep("change first collection name", () => Realm.Write(r => r.Remove(first))); + AddStep("remove first collection", () => Realm.Write(r => r.Remove(first))); assertCollectionCount(1); assertCollectionName(0, "2"); } @@ -154,8 +154,8 @@ namespace osu.Game.Tests.Visual.Collections }); AddStep("add two collections with same name", () => Realm.Write(r => r.Add(new[] { - new RealmBeatmapCollection(name: "1"), - new RealmBeatmapCollection(name: "1") + new BeatmapCollection(name: "1"), + new BeatmapCollection(name: "1") { BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } }, @@ -167,8 +167,8 @@ namespace osu.Game.Tests.Visual.Collections { AddStep("add two collections", () => Realm.Write(r => r.Add(new[] { - new RealmBeatmapCollection(name: "1"), - new RealmBeatmapCollection(name: "2") + new BeatmapCollection(name: "1"), + new BeatmapCollection(name: "2") { BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } }, @@ -207,7 +207,7 @@ namespace osu.Game.Tests.Visual.Collections { AddStep("add collection", () => Realm.Write(r => r.Add(new[] { - new RealmBeatmapCollection(name: "1") + new BeatmapCollection(name: "1") { BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } }, @@ -234,7 +234,7 @@ namespace osu.Game.Tests.Visual.Collections [Test] public void TestCollectionRenamedExternal() { - RealmBeatmapCollection first = null!; + BeatmapCollection first = null!; AddStep("add two collections", () => { @@ -242,8 +242,8 @@ namespace osu.Game.Tests.Visual.Collections { r.Add(new[] { - first = new RealmBeatmapCollection(name: "1"), - new RealmBeatmapCollection(name: "2"), + first = new BeatmapCollection(name: "1"), + new BeatmapCollection(name: "2"), }); }); }); @@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Collections [Test] public void TestCollectionRenamedOnTextChange() { - RealmBeatmapCollection first = null!; + BeatmapCollection first = null!; AddStep("add two collections", () => { @@ -264,8 +264,8 @@ namespace osu.Game.Tests.Visual.Collections { r.Add(new[] { - first = new RealmBeatmapCollection(name: "1"), - new RealmBeatmapCollection(name: "2"), + first = new BeatmapCollection(name: "1"), + new BeatmapCollection(name: "2"), }); }); }); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index e4d69334a3..2a4613c37b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.SongSelect [SetUp] public void SetUp() => Schedule(() => { - Realm.Write(r => r.RemoveAll()); + Realm.Write(r => r.RemoveAll()); Child = control = new FilterControl { @@ -69,8 +69,8 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestCollectionAddedToDropdown() { - AddStep("add collection", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "1")))); - AddStep("add collection", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "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"); } @@ -78,10 +78,10 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestCollectionRemovedFromDropdown() { - var first = new RealmBeatmapCollection(name: "1"); + var first = new BeatmapCollection(name: "1"); AddStep("add collection", () => Realm.Write(r => r.Add(first))); - AddStep("add collection", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "2")))); + AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "2")))); AddStep("remove collection", () => Realm.Write(r => r.Remove(first))); assertCollectionDropdownContains("1", false); @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestCollectionRenamed() { - AddStep("add collection", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "1")))); + AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1")))); AddStep("select collection", () => { var dropdown = control.ChildrenOfType().Single(); @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestCollectionFilterHasAddButton() { addExpandHeaderStep(); - AddStep("add collection", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "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); } @@ -128,7 +128,7 @@ namespace osu.Game.Tests.Visual.SongSelect { addExpandHeaderStep(); - AddStep("add collection", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "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); @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); - AddStep("add collection", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "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", () => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash)); @@ -161,7 +161,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); - AddStep("add collection", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "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); @@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.SongSelect { addExpandHeaderStep(); - AddStep("add collection", () => Realm.Write(r => r.Add(new RealmBeatmapCollection(name: "1")))); + AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1")))); AddStep("select collection", () => { InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1)); @@ -193,10 +193,10 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Click(MouseButton.Left); }); - AddAssert("collection filter still selected", () => control.CreateCriteria().Collection?.Name.Value == "1"); + AddAssert("collection filter still selected", () => control.CreateCriteria().Collection?.Name == "1"); } - private RealmBeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); + private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) => AddAssert($"collection dropdown header displays '{collectionName}'", diff --git a/osu.Game/Beatmaps/RealmBeatmapCollection.cs b/osu.Game/Beatmaps/RealmBeatmapCollection.cs deleted file mode 100644 index 22ba9d5789..0000000000 --- a/osu.Game/Beatmaps/RealmBeatmapCollection.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using osu.Game.Database; -using Realms; - -namespace osu.Game.Beatmaps -{ - public class RealmBeatmapCollection : RealmObject, IHasGuidPrimaryKey - { - [PrimaryKey] - public Guid ID { get; } - - public string Name { get; set; } = string.Empty; - - public IList BeatmapMD5Hashes { get; } = null!; - - /// - /// The date when this collection was last modified. - /// - public DateTimeOffset LastModified { get; set; } - - public RealmBeatmapCollection(string? name = null, IList? beatmapMD5Hashes = null) - { - ID = Guid.NewGuid(); - Name = name ?? string.Empty; - BeatmapMD5Hashes = beatmapMD5Hashes ?? new List(); - - LastModified = DateTimeOffset.UtcNow; - } - - [UsedImplicitly] - private RealmBeatmapCollection() - { - } - } -} diff --git a/osu.Game/Collections/BeatmapCollection.cs b/osu.Game/Collections/BeatmapCollection.cs index abfd0e6dd0..2ffe17d9e6 100644 --- a/osu.Game/Collections/BeatmapCollection.cs +++ b/osu.Game/Collections/BeatmapCollection.cs @@ -1,32 +1,50 @@ // Copyright (c) ppy Pty Ltd . 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 { /// /// A collection of beatmaps grouped by a name. /// - public class BeatmapCollection + public class BeatmapCollection : RealmObject, IHasGuidPrimaryKey { + [PrimaryKey] + public Guid ID { get; set; } + /// /// The collection's name. /// - public readonly Bindable Name = new Bindable(); + public string Name { get; set; } = string.Empty; /// /// The es of beatmaps contained by the collection. /// - public readonly BindableList BeatmapHashes = new BindableList(); + public IList BeatmapMD5Hashes { get; } = null!; /// /// The date when this collection was last modified. /// - public DateTimeOffset LastModifyDate { get; private set; } = DateTimeOffset.UtcNow; + public DateTimeOffset LastModified { get; set; } + + public BeatmapCollection(string? name = null, IList? beatmapMD5Hashes = null) + { + ID = Guid.NewGuid(); + Name = name ?? string.Empty; + BeatmapMD5Hashes = beatmapMD5Hashes ?? new List(); + + LastModified = DateTimeOffset.UtcNow; + } + + [UsedImplicitly] + private BeatmapCollection() + { + } } } diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index ed2c0c7cfb..1315cebc8b 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -1,12 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; 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; @@ -15,6 +13,7 @@ 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; @@ -43,8 +42,8 @@ namespace osu.Game.Collections private readonly IBindableList beatmaps = new BindableList(); private readonly BindableList filters = new BindableList(); - [Resolved(CanBeNull = true)] - private ManageCollectionsDialog manageCollectionsDialog { get; set; } + [Resolved] + private ManageCollectionsDialog? manageCollectionsDialog { get; set; } public CollectionFilterDropdown() { @@ -81,7 +80,7 @@ namespace osu.Game.Collections if (ShowManageCollectionsItem) filters.Add(new ManageCollectionsFilterMenuItem()); - Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection == selectedItem) ?? filters[0]; + Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection.ID == selectedItem?.ID) ?? filters[0]; } /// @@ -92,11 +91,12 @@ namespace osu.Game.Collections // 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); + // TODO: binding with realm + // if (filter.OldValue?.Collection != null) + // beatmaps.UnbindFrom(filter.OldValue.Collection.BeatmapMD5Hashes); + // + // if (filter.NewValue?.Collection != null) + // beatmaps.BindTo(filter.NewValue.Collection.BeatmapMD5Hashes); beatmaps.CollectionChanged += filterBeatmapsChanged; @@ -187,26 +187,24 @@ namespace osu.Game.Collections protected class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem { - [NotNull] protected new CollectionFilterMenuItem Item => ((DropdownMenuItem)base.Item).Value; [Resolved] - private IBindable beatmap { get; set; } + private IBindable beatmap { get; set; } = null!; - [CanBeNull] - private readonly BindableList collectionBeatmaps; - - [NotNull] private readonly Bindable collectionName; - private IconButton addOrRemoveButton; - private Content content; + private IconButton addOrRemoveButton = null!; + private Content content = null!; private bool beatmapInCollection; + private IDisposable? realmSubscription; + + private BeatmapCollection? collection => Item.Collection; + public CollectionDropdownMenuItem(MenuItem item) : base(item) { - collectionBeatmaps = Item.Collection?.BeatmapHashes.GetBoundCopy(); collectionName = Item.CollectionName.GetBoundCopy(); } @@ -223,14 +221,17 @@ namespace osu.Game.Collections }); } + [Resolved] + private RealmAccess realm { get; set; } = null!; + protected override void LoadComplete() { base.LoadComplete(); - if (collectionBeatmaps != null) + if (Item.Collection != null) { - collectionBeatmaps.CollectionChanged += (_, _) => collectionChanged(); - beatmap.BindValueChanged(_ => collectionChanged(), true); + realmSubscription = realm.SubscribeToPropertyChanged(r => r.Find(Item.Collection.ID), c => c.BeatmapMD5Hashes, _ => hashesChanged()); + beatmap.BindValueChanged(_ => hashesChanged(), 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 @@ -252,11 +253,11 @@ namespace osu.Game.Collections base.OnHoverLost(e); } - private void collectionChanged() + private void hashesChanged() { - Debug.Assert(collectionBeatmaps != null); + Debug.Assert(collection != null); - beatmapInCollection = collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo.MD5Hash); + beatmapInCollection = collection.BeatmapMD5Hashes.Contains(beatmap.Value.BeatmapInfo.MD5Hash); addOrRemoveButton.Enabled.Value = !beatmap.IsDefault; addOrRemoveButton.Icon = beatmapInCollection ? FontAwesome.Solid.MinusSquare : FontAwesome.Solid.PlusSquare; @@ -273,7 +274,7 @@ namespace osu.Game.Collections private void updateButtonVisibility() { - if (collectionBeatmaps == null) + if (collection == null) addOrRemoveButton.Alpha = 0; else addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0; @@ -281,13 +282,22 @@ namespace osu.Game.Collections private void addOrRemove() { - Debug.Assert(collectionBeatmaps != null); + Debug.Assert(collection != null); - if (!collectionBeatmaps.Remove(beatmap.Value.BeatmapInfo.MD5Hash)) - collectionBeatmaps.Add(beatmap.Value.BeatmapInfo.MD5Hash); + realm.Write(r => + { + if (!collection.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash)) + collection.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash); + }); } protected override Drawable CreateContent() => content = (Content)base.CreateContent(); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + realmSubscription?.Dispose(); + } } } } diff --git a/osu.Game/Collections/CollectionFilterMenuItem.cs b/osu.Game/Collections/CollectionFilterMenuItem.cs index 031f05c0b4..4c132ba7b7 100644 --- a/osu.Game/Collections/CollectionFilterMenuItem.cs +++ b/osu.Game/Collections/CollectionFilterMenuItem.cs @@ -1,10 +1,7 @@ // Copyright (c) ppy Pty Ltd . 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; namespace osu.Game.Collections @@ -18,26 +15,26 @@ namespace osu.Game.Collections /// The collection to filter beatmaps from. /// May be null to not filter by collection (include all beatmaps). /// - [CanBeNull] - public readonly BeatmapCollection Collection; + public readonly BeatmapCollection? Collection; /// /// The name of the collection. /// - [NotNull] public readonly Bindable CollectionName; /// /// Creates a new . /// /// The collection to filter beatmaps from. - public CollectionFilterMenuItem([CanBeNull] BeatmapCollection collection) + public CollectionFilterMenuItem(BeatmapCollection? collection) { Collection = collection; - CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable("All beatmaps"); + CollectionName = new Bindable(collection?.Name ?? "All beatmaps"); } - public bool Equals(CollectionFilterMenuItem other) + // TODO: track name changes i guess? + + public bool Equals(CollectionFilterMenuItem? other) { if (other == null) return false; @@ -45,7 +42,7 @@ 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). diff --git a/osu.Game/Collections/CollectionToggleMenuItem.cs b/osu.Game/Collections/CollectionToggleMenuItem.cs index 632249913d..8c0e3c587b 100644 --- a/osu.Game/Collections/CollectionToggleMenuItem.cs +++ b/osu.Game/Collections/CollectionToggleMenuItem.cs @@ -8,7 +8,7 @@ namespace osu.Game.Collections { public class CollectionToggleMenuItem : ToggleMenuItem { - public CollectionToggleMenuItem(RealmBeatmapCollection collection, IBeatmapInfo beatmap) + public CollectionToggleMenuItem(BeatmapCollection collection, IBeatmapInfo beatmap) : base(collection.Name, MenuItemType.Standard, state => { if (state) diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs index 33c2174623..7594978870 100644 --- a/osu.Game/Collections/DeleteCollectionDialog.cs +++ b/osu.Game/Collections/DeleteCollectionDialog.cs @@ -1,19 +1,16 @@ // Copyright (c) ppy Pty Ltd . 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.Beatmaps; using osu.Game.Overlays.Dialog; namespace osu.Game.Collections { public class DeleteCollectionDialog : PopupDialog { - public DeleteCollectionDialog(RealmBeatmapCollection collection, Action deleteAction) + public DeleteCollectionDialog(BeatmapCollection collection, Action deleteAction) { HeaderText = "Confirm deletion of"; BodyText = $"{collection.Name} ({"beatmap".ToQuantity(collection.BeatmapMD5Hashes.Count)})"; diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 63f04641f4..f376d18224 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -1,35 +1,33 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osuTK; namespace osu.Game.Collections { /// - /// Visualises a list of s. + /// Visualises a list of s. /// - public class DrawableCollectionList : OsuRearrangeableListContainer + public class DrawableCollectionList : OsuRearrangeableListContainer { - private Scroll scroll; + private Scroll scroll = null!; protected override ScrollContainer CreateScrollContainer() => scroll = new Scroll(); - protected override FillFlowContainer> CreateListFillFlowContainer() => new Flow + protected override FillFlowContainer> CreateListFillFlowContainer() => new Flow { DragActive = { BindTarget = DragActive } }; // TODO: source from realm - protected override OsuRearrangeableListItem CreateOsuDrawable(RealmBeatmapCollection item) + protected override OsuRearrangeableListItem CreateOsuDrawable(BeatmapCollection item) { if (item.ID == scroll.PlaceholderItem.Model.ID) return scroll.ReplacePlaceholder(); @@ -49,7 +47,7 @@ namespace osu.Game.Collections /// /// The currently-displayed placeholder item. /// - public DrawableCollectionListItem PlaceholderItem { get; private set; } + public DrawableCollectionListItem PlaceholderItem { get; private set; } = null!; protected override Container Content => content; private readonly Container content; @@ -79,6 +77,7 @@ namespace osu.Game.Collections }); ReplacePlaceholder(); + Debug.Assert(PlaceholderItem != null); } protected override void Update() @@ -98,7 +97,7 @@ namespace osu.Game.Collections var previous = PlaceholderItem; placeholderContainer.Clear(false); - placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new RealmBeatmapCollection(), false)); + placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection(), false)); return previous; } @@ -107,7 +106,7 @@ namespace osu.Game.Collections /// /// The flow of . Disables layout easing unless a drag is in progress. /// - private class Flow : FillFlowContainer> + private class Flow : FillFlowContainer> { public readonly IBindable DragActive = new Bindable(); diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index a29b2ef81c..6093e69deb 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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; @@ -12,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; -using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -24,15 +21,15 @@ using osuTK.Graphics; namespace osu.Game.Collections { /// - /// Visualises a inside a . + /// Visualises a inside a . /// - public class DrawableCollectionListItem : OsuRearrangeableListItem + public class DrawableCollectionListItem : OsuRearrangeableListItem { private const float item_height = 35; private const float button_width = item_height * 0.75f; /// - /// Whether the currently exists inside realm. + /// Whether the currently exists inside realm. /// public IBindable IsCreated => isCreated; @@ -41,9 +38,9 @@ namespace osu.Game.Collections /// /// Creates a new . /// - /// The . + /// The . /// Whether currently exists inside realm. - public DrawableCollectionListItem(RealmBeatmapCollection item, bool isCreated) + public DrawableCollectionListItem(BeatmapCollection item, bool isCreated) : base(item) { this.isCreated.Value = isCreated; @@ -63,12 +60,15 @@ namespace osu.Game.Collections { public readonly Bindable IsCreated = new Bindable(); - private readonly RealmBeatmapCollection collection; + private readonly BeatmapCollection collection; - private Container textBoxPaddingContainer; - private ItemTextBox textBox; + private Container textBoxPaddingContainer = null!; + private ItemTextBox textBox = null!; - public ItemContent(RealmBeatmapCollection collection) + [Resolved] + private RealmAccess realm { get; set; } = null!; + + public ItemContent(BeatmapCollection collection) { this.collection = collection; @@ -107,9 +107,6 @@ namespace osu.Game.Collections }; } - [Resolved] - private RealmAccess realm { get; set; } - protected override void LoadComplete() { base.LoadComplete(); @@ -156,20 +153,20 @@ namespace osu.Game.Collections { public readonly IBindable IsCreated = new Bindable(); - public Func IsTextBoxHovered; - - [Resolved(CanBeNull = true)] - private IDialogOverlay dialogOverlay { get; set; } + public Func IsTextBoxHovered = null!; [Resolved] - private RealmAccess realmAccess { get; set; } + private IDialogOverlay? dialogOverlay { get; set; } - private readonly RealmBeatmapCollection collection; + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; - private Drawable fadeContainer; - private Drawable background; + private readonly BeatmapCollection collection; - public DeleteButton(RealmBeatmapCollection collection) + private Drawable fadeContainer = null!; + private Drawable background = null!; + + public DeleteButton(BeatmapCollection collection) { this.collection = collection; RelativeSizeAxes = Axes.Y; diff --git a/osu.Game/Database/LegacyCollectionImporter.cs b/osu.Game/Database/LegacyCollectionImporter.cs index aa98c491b1..bd32b8c446 100644 --- a/osu.Game/Database/LegacyCollectionImporter.cs +++ b/osu.Game/Database/LegacyCollectionImporter.cs @@ -7,7 +7,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using osu.Framework.Logging; -using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.IO; using osu.Game.IO.Legacy; using osu.Game.Overlays.Notifications; @@ -75,7 +75,7 @@ namespace osu.Game.Database notification.State = ProgressNotificationState.Completed; } - private Task importCollections(List newCollections) + private Task importCollections(List newCollections) { var tcs = new TaskCompletionSource(); @@ -85,7 +85,7 @@ namespace osu.Game.Database { foreach (var collection in newCollections) { - var existing = r.All().FirstOrDefault(c => c.Name == collection.Name); + var existing = r.All().FirstOrDefault(c => c.Name == collection.Name); if (existing != null) { @@ -111,7 +111,7 @@ namespace osu.Game.Database return tcs.Task; } - private List readCollections(Stream stream, ProgressNotification? notification = null) + private List readCollections(Stream stream, ProgressNotification? notification = null) { if (notification != null) { @@ -119,7 +119,7 @@ namespace osu.Game.Database notification.Progress = 0; } - var result = new List(); + var result = new List(); try { @@ -135,7 +135,7 @@ namespace osu.Game.Database if (notification?.CancellationToken.IsCancellationRequested == true) return result; - var collection = new RealmBeatmapCollection(sr.ReadString()); + var collection = new BeatmapCollection(sr.ReadString()); int mapCount = sr.ReadInt32(); for (int j = 0; j < mapCount; j++) diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs index 954c4de493..19b1b3f84e 100644 --- a/osu.Game/Overlays/Music/Playlist.cs +++ b/osu.Game/Overlays/Music/Playlist.cs @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Music else { item.InSelectedCollection = item.Model.Value.Beatmaps.Select(b => b.MD5Hash) - .Any(criteria.Collection.BeatmapHashes.Contains); + .Any(criteria.Collection.BeatmapMD5Hashes.Contains); } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs index 0b17ab9c6c..498859db46 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs @@ -3,7 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Localisation; -using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Localisation; using osu.Game.Overlays.Notifications; @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private void deleteAllCollections() { - realm.Write(r => r.RemoveAll()); + realm.Write(r => r.RemoveAll()); notificationOverlay.Post(new ProgressCompletionNotification { Text = "Deleted all collections!" }); } } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index b17c4934cd..0826c4144b 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -499,7 +499,7 @@ namespace osu.Game.Screens.OnlinePlay { if (beatmaps.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID) is BeatmapInfo local && !local.BeatmapSet.AsNonNull().DeletePending) { - var collectionItems = realm.Realm.All().Select(c => new CollectionToggleMenuItem(c, beatmap)).Cast().ToList(); + var collectionItems = realm.Realm.All().Select(c => new CollectionToggleMenuItem(c, beatmap)).Cast().ToList(); if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 81734745c4..9267434a66 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -76,7 +76,7 @@ namespace osu.Game.Screens.Select.Carousel } if (match) - match &= criteria.Collection?.BeatmapHashes.Contains(BeatmapInfo.MD5Hash) ?? true; + match &= criteria.Collection?.BeatmapMD5Hashes.Contains(BeatmapInfo.MD5Hash) ?? true; if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index bfc93b34e2..a1c433b440 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -238,7 +238,7 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID))); - var collectionItems = realm.Realm.All().Select(c => new CollectionToggleMenuItem(c, beatmapInfo)).Cast().ToList(); + var collectionItems = realm.Realm.All().AsEnumerable().Select(c => new CollectionToggleMenuItem(c, beatmapInfo)).Cast().ToList(); if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 3726d955bd..75c9daf1b1 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -224,7 +224,7 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapSet.OnlineID > 0 && viewDetails != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID))); - var collectionItems = realm.Realm.All().AsEnumerable().Select(createCollectionMenuItem).ToList(); + var collectionItems = realm.Realm.All().AsEnumerable().Select(createCollectionMenuItem).ToList(); if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); @@ -239,7 +239,7 @@ namespace osu.Game.Screens.Select.Carousel } } - private MenuItem createCollectionMenuItem(RealmBeatmapCollection collection) + private MenuItem createCollectionMenuItem(BeatmapCollection collection) { Debug.Assert(beatmapSet != null); From 438067a18b4748501c49a57db0201212eb22829c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 17:17:43 +0900 Subject: [PATCH 04/17] Convert realm data propagation to more correctly use `Live` wip --- .../SongSelect/TestSceneFilterControl.cs | 8 +++--- .../Collections/CollectionFilterDropdown.cs | 13 ++++----- .../Collections/CollectionFilterMenuItem.cs | 7 ++--- .../Collections/CollectionToggleMenuItem.cs | 18 ++++++++----- osu.Game/Overlays/Music/FilterCriteria.cs | 3 ++- osu.Game/Overlays/Music/Playlist.cs | 6 +++-- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- .../Select/Carousel/CarouselBeatmap.cs | 2 +- .../Carousel/DrawableCarouselBeatmap.cs | 2 +- .../Carousel/DrawableCarouselBeatmapSet.cs | 27 +++++++++++-------- osu.Game/Screens/Select/FilterControl.cs | 2 +- osu.Game/Screens/Select/FilterCriteria.cs | 4 +-- 12 files changed, 54 insertions(+), 40 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 2a4613c37b..aaaa0aec95 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -147,10 +147,10 @@ namespace osu.Game.Tests.Visual.SongSelect 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", () => getFirstCollection().BeatmapMD5Hashes.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", () => getFirstCollection().BeatmapMD5Hashes.Clear()); + AddStep("remove beatmap from collection", () => Realm.Write(r => getFirstCollection().BeatmapMD5Hashes.Clear())); AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); } @@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.SongSelect { addExpandHeaderStep(); - AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1")))); + AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1", new List { "abc" })))); AddStep("select collection", () => { InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1)); @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Click(MouseButton.Left); }); - AddAssert("collection filter still selected", () => control.CreateCriteria().Collection?.Name == "1"); + AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes.Any()); } private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index 1315cebc8b..c04f617eb9 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -38,7 +38,7 @@ namespace osu.Game.Collections set => current.Current = value; } - private readonly IBindableList collections = new BindableList(); + private readonly IBindableList> collections = new BindableList>(); private readonly IBindableList beatmaps = new BindableList(); private readonly BindableList filters = new BindableList(); @@ -114,6 +114,7 @@ namespace osu.Game.Collections /// private void filterBeatmapsChanged(object sender, NotifyCollectionChangedEventArgs e) { + // TODO: fuck this shit right off // 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(); @@ -200,7 +201,7 @@ namespace osu.Game.Collections private IDisposable? realmSubscription; - private BeatmapCollection? collection => Item.Collection; + private Live? collection => Item.Collection; public CollectionDropdownMenuItem(MenuItem item) : base(item) @@ -257,7 +258,7 @@ namespace osu.Game.Collections { Debug.Assert(collection != null); - beatmapInCollection = collection.BeatmapMD5Hashes.Contains(beatmap.Value.BeatmapInfo.MD5Hash); + 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; @@ -284,10 +285,10 @@ namespace osu.Game.Collections { Debug.Assert(collection != null); - realm.Write(r => + collection.PerformWrite(c => { - if (!collection.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash)) - collection.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash); + if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash)) + c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash); }); } diff --git a/osu.Game/Collections/CollectionFilterMenuItem.cs b/osu.Game/Collections/CollectionFilterMenuItem.cs index 4c132ba7b7..fd9e333915 100644 --- a/osu.Game/Collections/CollectionFilterMenuItem.cs +++ b/osu.Game/Collections/CollectionFilterMenuItem.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Bindables; +using osu.Game.Database; namespace osu.Game.Collections { @@ -15,7 +16,7 @@ namespace osu.Game.Collections /// The collection to filter beatmaps from. /// May be null to not filter by collection (include all beatmaps). /// - public readonly BeatmapCollection? Collection; + public readonly Live? Collection; /// /// The name of the collection. @@ -26,10 +27,10 @@ namespace osu.Game.Collections /// Creates a new . /// /// The collection to filter beatmaps from. - public CollectionFilterMenuItem(BeatmapCollection? collection) + public CollectionFilterMenuItem(Live? collection) { Collection = collection; - CollectionName = new Bindable(collection?.Name ?? "All beatmaps"); + CollectionName = new Bindable(collection?.PerformRead(c => c.Name) ?? "All beatmaps"); } // TODO: track name changes i guess? diff --git a/osu.Game/Collections/CollectionToggleMenuItem.cs b/osu.Game/Collections/CollectionToggleMenuItem.cs index 8c0e3c587b..5ad06a72c0 100644 --- a/osu.Game/Collections/CollectionToggleMenuItem.cs +++ b/osu.Game/Collections/CollectionToggleMenuItem.cs @@ -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, MenuItemType.Standard, state => + public CollectionToggleMenuItem(Live collection, IBeatmapInfo beatmap) + : base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state => { - if (state) - collection.BeatmapMD5Hashes.Add(beatmap.MD5Hash); - else - collection.BeatmapMD5Hashes.Remove(beatmap.MD5Hash); + collection.PerformWrite(c => + { + if (state) + c.BeatmapMD5Hashes.Add(beatmap.MD5Hash); + else + c.BeatmapMD5Hashes.Remove(beatmap.MD5Hash); + }); }) { - State.Value = collection.BeatmapMD5Hashes.Contains(beatmap.MD5Hash); + State.Value = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.MD5Hash)); } } } diff --git a/osu.Game/Overlays/Music/FilterCriteria.cs b/osu.Game/Overlays/Music/FilterCriteria.cs index f435c4e6e4..ad491be845 100644 --- a/osu.Game/Overlays/Music/FilterCriteria.cs +++ b/osu.Game/Overlays/Music/FilterCriteria.cs @@ -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. /// [CanBeNull] - public BeatmapCollection Collection; + public Live Collection; } } diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs index 19b1b3f84e..2bb0ff1085 100644 --- a/osu.Game/Overlays/Music/Playlist.cs +++ b/osu.Game/Overlays/Music/Playlist.cs @@ -31,14 +31,16 @@ namespace osu.Game.Overlays.Music { var items = (SearchContainer>>)ListContainer; + string[] currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray()); + foreach (var item in items.OfType()) { - if (criteria.Collection == null) + if (currentCollectionHashes == null) item.InSelectedCollection = true; else { item.InSelectedCollection = item.Model.Value.Beatmaps.Select(b => b.MD5Hash) - .Any(criteria.Collection.BeatmapMD5Hashes.Contains); + .Any(currentCollectionHashes.Contains); } } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 0826c4144b..492bb8ada5 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -499,7 +499,7 @@ namespace osu.Game.Screens.OnlinePlay { if (beatmaps.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID) is BeatmapInfo local && !local.BeatmapSet.AsNonNull().DeletePending) { - var collectionItems = realm.Realm.All().Select(c => new CollectionToggleMenuItem(c, beatmap)).Cast().ToList(); + var collectionItems = realm.Realm.All().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast().ToList(); if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 9267434a66..5b17b412ae 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -76,7 +76,7 @@ namespace osu.Game.Screens.Select.Carousel } if (match) - match &= criteria.Collection?.BeatmapMD5Hashes.Contains(BeatmapInfo.MD5Hash) ?? true; + match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true; if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index a1c433b440..c3cb04680b 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -238,7 +238,7 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID))); - var collectionItems = realm.Realm.All().AsEnumerable().Select(c => new CollectionToggleMenuItem(c, beatmapInfo)).Cast().ToList(); + var collectionItems = realm.Realm.All().AsEnumerable().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast().ToList(); if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 75c9daf1b1..040f954bba 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -254,24 +254,29 @@ namespace osu.Game.Screens.Select.Carousel else state = TernaryState.False; + 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.BeatmapMD5Hashes.Contains(b.MD5Hash)) - continue; + switch (s) + { + case TernaryState.True: + if (c.BeatmapMD5Hashes.Contains(b.MD5Hash)) + continue; - collection.BeatmapMD5Hashes.Add(b.MD5Hash); - break; + c.BeatmapMD5Hashes.Add(b.MD5Hash); + break; - case TernaryState.False: - collection.BeatmapMD5Hashes.Remove(b.MD5Hash); - break; + case TernaryState.False: + c.BeatmapMD5Hashes.Remove(b.MD5Hash); + break; + } } - } + }); }) { State = { Value = state } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index d39862b65f..5f5344a338 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -49,7 +49,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) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index c7e6e8496a..320bfb1b45 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -68,10 +68,10 @@ namespace osu.Game.Screens.Select } /// - /// The collection to filter beatmaps from. + /// Hashes from the to filter to. /// [CanBeNull] - public BeatmapCollection Collection; + public IEnumerable CollectionBeatmapMD5Hashes { get; set; } [CanBeNull] public IRulesetFilterCriteria RulesetCriteria { get; set; } From 804bb33aedbc4f44a8aed6fc7c90d7528106b1df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 18:24:37 +0900 Subject: [PATCH 05/17] Hook up remaining data flows --- .../Collections/CollectionFilterDropdown.cs | 147 +++++------------- .../Collections/CollectionFilterMenuItem.cs | 21 +-- 2 files changed, 54 insertions(+), 114 deletions(-) diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index c04f617eb9..ec409df4d6 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -17,6 +16,7 @@ using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osuTK; +using Realms; namespace osu.Game.Collections { @@ -38,13 +38,15 @@ namespace osu.Game.Collections set => current.Current = value; } - private readonly IBindableList> collections = new BindableList>(); private readonly IBindableList beatmaps = new BindableList(); private readonly BindableList filters = new BindableList(); [Resolved] private ManageCollectionsDialog? manageCollectionsDialog { get; set; } + [Resolved] + private RealmAccess realm { get; set; } = null!; + public CollectionFilterDropdown() { ItemSource = filters; @@ -55,51 +57,49 @@ namespace osu.Game.Collections { base.LoadComplete(); - // TODO: bind to realm data + realm.RegisterForNotifications(r => r.All(), collectionsChanged); // 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); + Current.BindValueChanged(currentChanged, true); } /// /// Occurs when a collection has been added or removed. /// - private void collectionsChanged() + private void collectionsChanged(IRealmCollection collections, ChangeSet? changes, Exception error) { var selectedItem = SelectedItem?.Value?.Collection; filters.Clear(); filters.Add(new AllBeatmapsCollectionFilterMenuItem()); - filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c))); + filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm)))); if (ShowManageCollectionsItem) filters.Add(new ManageCollectionsFilterMenuItem()); 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 changeset. + if (selectedItem != null && changes != null) + { + foreach (int index in changes.ModifiedIndices) + { + if (collections[index].ID == selectedItem.ID) + { + // 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(); + } + } + } } - /// - /// Occurs when the selection has changed. - /// - private void filterChanged(ValueChangedEvent filter) + private void currentChanged(ValueChangedEvent 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; - - // TODO: binding with realm - // if (filter.OldValue?.Collection != null) - // beatmaps.UnbindFrom(filter.OldValue.Collection.BeatmapMD5Hashes); - // - // if (filter.NewValue?.Collection != null) - // beatmaps.BindTo(filter.NewValue.Collection.BeatmapMD5Hashes); - - 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) @@ -109,18 +109,7 @@ namespace osu.Game.Collections } } - /// - /// Occurs when the beatmaps contained by a have changed. - /// - private void filterBeatmapsChanged(object sender, NotifyCollectionChangedEventArgs e) - { - // TODO: fuck this shit right off - // 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 override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName; protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader().With(d => { @@ -136,13 +125,6 @@ namespace osu.Game.Collections public class CollectionDropdownHeader : OsuDropdownHeader { public readonly Bindable SelectedItem = new Bindable(); - private readonly Bindable collectionName = new Bindable(); - - protected override LocalisableString Label - { - get => base.Label; - set { } // See updateText(). - } public CollectionDropdownHeader() { @@ -150,26 +132,6 @@ namespace osu.Game.Collections 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 @@ -190,23 +152,16 @@ namespace osu.Game.Collections { protected new CollectionFilterMenuItem Item => ((DropdownMenuItem)base.Item).Value; - [Resolved] - private IBindable beatmap { get; set; } = null!; - - private readonly Bindable collectionName; - private IconButton addOrRemoveButton = null!; - private Content content = null!; + private bool beatmapInCollection; - private IDisposable? realmSubscription; - - private Live? collection => Item.Collection; + [Resolved] + private IBindable beatmap { get; set; } = null!; public CollectionDropdownMenuItem(MenuItem item) : base(item) { - collectionName = Item.CollectionName.GetBoundCopy(); } [BackgroundDependencyLoader] @@ -222,22 +177,25 @@ namespace osu.Game.Collections }); } - [Resolved] - private RealmAccess realm { get; set; } = null!; - protected override void LoadComplete() { base.LoadComplete(); if (Item.Collection != null) { - realmSubscription = realm.SubscribeToPropertyChanged(r => r.Find(Item.Collection.ID), c => c.BeatmapMD5Hashes, _ => hashesChanged()); - beatmap.BindValueChanged(_ => hashesChanged(), true); - } + beatmap.BindValueChanged(_ => + { + Debug.Assert(Item.Collection != null); - // 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); + beatmapInCollection = Item.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(); } @@ -254,19 +212,6 @@ namespace osu.Game.Collections base.OnHoverLost(e); } - private void hashesChanged() - { - Debug.Assert(collection != null); - - 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(); - } - protected override void OnSelectChange() { base.OnSelectChange(); @@ -275,7 +220,7 @@ namespace osu.Game.Collections private void updateButtonVisibility() { - if (collection == null) + if (Item.Collection == null) addOrRemoveButton.Alpha = 0; else addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0; @@ -283,22 +228,16 @@ namespace osu.Game.Collections private void addOrRemove() { - Debug.Assert(collection != null); + Debug.Assert(Item.Collection != null); - collection.PerformWrite(c => + Item.Collection.PerformWrite(c => { if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash)) c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash); }); } - protected override Drawable CreateContent() => content = (Content)base.CreateContent(); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - realmSubscription?.Dispose(); - } + protected override Drawable CreateContent() => (Content)base.CreateContent(); } } } diff --git a/osu.Game/Collections/CollectionFilterMenuItem.cs b/osu.Game/Collections/CollectionFilterMenuItem.cs index fd9e333915..2ac5784f09 100644 --- a/osu.Game/Collections/CollectionFilterMenuItem.cs +++ b/osu.Game/Collections/CollectionFilterMenuItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Bindables; using osu.Game.Database; namespace osu.Game.Collections @@ -21,19 +20,22 @@ namespace osu.Game.Collections /// /// The name of the collection. /// - public readonly Bindable CollectionName; + public string CollectionName { get; } /// /// Creates a new . /// /// The collection to filter beatmaps from. - public CollectionFilterMenuItem(Live? collection) + public CollectionFilterMenuItem(Live collection) + : this(collection.PerformRead(c => c.Name)) { Collection = collection; - CollectionName = new Bindable(collection?.PerformRead(c => c.Name) ?? "All beatmaps"); } - // TODO: track name changes i guess? + protected CollectionFilterMenuItem(string name) + { + CollectionName = name; + } public bool Equals(CollectionFilterMenuItem? other) { @@ -47,16 +49,16 @@ namespace osu.Game.Collections // 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") { } } @@ -64,9 +66,8 @@ namespace osu.Game.Collections public class ManageCollectionsFilterMenuItem : CollectionFilterMenuItem { public ManageCollectionsFilterMenuItem() - : base(null) + : base("Manage collections...") { - CollectionName.Value = "Manage collections..."; } } } From 67c7f324ee899d652e127903a82917c980896e95 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 19:02:43 +0900 Subject: [PATCH 06/17] Simplify `CollectionFilterDropdown` filter flow weirdness --- .../SongSelect/TestSceneFilterControl.cs | 10 +++++ .../Collections/CollectionFilterDropdown.cs | 43 +++++++------------ osu.Game/Screens/Select/FilterControl.cs | 20 +++------ 3 files changed, 32 insertions(+), 41 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index aaaa0aec95..feb71def5d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -176,6 +176,8 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestManageCollectionsFilterIsNotSelected() { + bool received = false; + addExpandHeaderStep(); AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1", new List { "abc" })))); @@ -187,6 +189,12 @@ namespace osu.Game.Tests.Visual.SongSelect addExpandHeaderStep(); + AddStep("watch for filter requests", () => + { + received = false; + control.ChildrenOfType().First().RequestFilter = () => received = true; + }); + AddStep("click manage collections filter", () => { InputManager.MoveMouseTo(getCollectionDropdownItems().Last()); @@ -194,6 +202,8 @@ namespace osu.Game.Tests.Visual.SongSelect }); AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes.Any()); + + AddAssert("filter request not fired", () => !received); } private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index ec409df4d6..fa15e2b56f 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -30,15 +30,8 @@ namespace osu.Game.Collections /// protected virtual bool ShowManageCollectionsItem => true; - private readonly BindableWithCurrent current = new BindableWithCurrent(); + public Action? RequestFilter { private get; set; } - public new Bindable Current - { - get => current.Current; - set => current.Current = value; - } - - private readonly IBindableList beatmaps = new BindableList(); private readonly BindableList filters = new BindableList(); [Resolved] @@ -50,6 +43,7 @@ namespace osu.Game.Collections public CollectionFilterDropdown() { ItemSource = filters; + Current.Value = new AllBeatmapsCollectionFilterMenuItem(); } @@ -59,17 +53,9 @@ namespace osu.Game.Collections realm.RegisterForNotifications(r => r.All(), collectionsChanged); - // 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; - - Current.BindValueChanged(currentChanged, true); + Current.BindValueChanged(currentChanged); } - /// - /// Occurs when a collection has been added or removed. - /// private void collectionsChanged(IRealmCollection collections, ChangeSet? changes, Exception error) { var selectedItem = SelectedItem?.Value?.Collection; @@ -83,38 +69,41 @@ namespace osu.Game.Collections 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 changeset. + // 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) - { - // 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(); - } + RequestFilter?.Invoke(); } } } private void currentChanged(ValueChangedEvent 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; } + + // This dropdown be weird. + // We only care about filtering if the actual collection has changed. + if (filter.OldValue?.Collection != null || filter.NewValue?.Collection != null) + RequestFilter?.Invoke(); } protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName; - protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader().With(d => - { - d.SelectedItem.BindTarget = Current; - }); + protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader().With(d => d.SelectedItem.BindTarget = Current); protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu(); diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 5f5344a338..f07817d5dc 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -39,6 +39,10 @@ namespace osu.Game.Screens.Select private Bindable groupMode; + private SeekLimitedSearchTextBox searchTextBox; + + private CollectionFilterDropdown 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, - CollectionBeatmapMD5Hashes = collectionDropdown?.Current.Value?.Collection?.PerformRead(c => c.BeatmapMD5Hashes) + 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); @@ -183,6 +183,7 @@ namespace osu.Game.Screens.Select { 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(); From 34a2d1a6e1921baa76a8521ffbfa7d3b4d30f7fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 19:35:25 +0900 Subject: [PATCH 07/17] Fix `ManageCollectionsDialog` and remove weird placeholder logic --- .../TestSceneManageCollectionsDialog.cs | 27 +++++- .../Collections/CollectionFilterDropdown.cs | 33 ++++--- .../Collections/DeleteCollectionDialog.cs | 5 +- .../Collections/DrawableCollectionList.cs | 30 ++++-- .../Collections/DrawableCollectionListItem.cs | 97 ++++++------------- .../Collections/ManageCollectionsDialog.cs | 4 +- 6 files changed, 103 insertions(+), 93 deletions(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 8de38eb4e7..afcb511a6a 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -109,10 +109,15 @@ 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.PerformWrite(c => c.Name = "a")); + assertCollectionCount(0); + + AddStep("change collection name", () => + { + placeholderItem.ChildrenOfType().First().Text = "test text"; + InputManager.Key(Key.Enter); + }); + assertCollectionCount(1); - AddAssert("collection now exists", () => placeholderItem.Model.IsManaged); AddAssert("last item is placeholder", () => !dialog.ChildrenOfType().Last().Model.IsManaged); } @@ -257,6 +262,7 @@ namespace osu.Game.Tests.Visual.Collections public void TestCollectionRenamedOnTextChange() { BeatmapCollection first = null!; + DrawableCollectionListItem firstItem = null!; AddStep("add two collections", () => { @@ -272,12 +278,23 @@ namespace osu.Game.Tests.Visual.Collections assertCollectionCount(2); - AddStep("change first collection name", () => dialog.ChildrenOfType().First().Text = "First"); + AddStep("focus first collection", () => + { + InputManager.MoveMouseTo(firstItem = dialog.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("change first collection name", () => + { + firstItem.ChildrenOfType().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().Count(i => i.IsCreated.Value) == count); + => AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType().Count() == count + 1); // +1 for placeholder private void assertCollectionName(int index, string name) => AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType().ElementAt(index).ChildrenOfType().First().Text == name); diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index fa15e2b56f..790a23d2e5 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -53,21 +53,27 @@ namespace osu.Game.Collections realm.RegisterForNotifications(r => r.All(), collectionsChanged); - Current.BindValueChanged(currentChanged); + Current.BindValueChanged(selectionChanged); } private void collectionsChanged(IRealmCollection collections, ChangeSet? changes, Exception error) { var selectedItem = SelectedItem?.Value?.Collection; + var allBeatmaps = new AllBeatmapsCollectionFilterMenuItem(); + filters.Clear(); - filters.Add(new AllBeatmapsCollectionFilterMenuItem()); + filters.Add(allBeatmaps); filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm)))); if (ShowManageCollectionsItem) filters.Add(new ManageCollectionsFilterMenuItem()); - Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection.ID == selectedItem?.ID) ?? filters[0]; + // 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) @@ -80,7 +86,9 @@ namespace osu.Game.Collections } } - private void currentChanged(ValueChangedEvent filter) + private Live? lastFiltered; + + private void selectionChanged(ValueChangedEvent filter) { // May be null during .Clear(). if (filter.NewValue == null) @@ -95,15 +103,20 @@ namespace osu.Game.Collections return; } + var newCollection = filter.NewValue?.Collection; + // This dropdown be weird. // We only care about filtering if the actual collection has changed. - if (filter.OldValue?.Collection != null || filter.NewValue?.Collection != null) + if (newCollection != lastFiltered) + { RequestFilter?.Invoke(); + lastFiltered = newCollection; + } } protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName; - protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader().With(d => d.SelectedItem.BindTarget = Current); + protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader(); protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu(); @@ -113,8 +126,6 @@ namespace osu.Game.Collections public class CollectionDropdownHeader : OsuDropdownHeader { - public readonly Bindable SelectedItem = new Bindable(); - public CollectionDropdownHeader() { Height = 25; @@ -130,14 +141,14 @@ namespace osu.Game.Collections MaxHeight = 200; } - protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item) + protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownDrawableMenuItem(item) { BackgroundColourHover = HoverColour, BackgroundColourSelected = SelectionColour }; } - protected class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem + protected class CollectionDropdownDrawableMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem { protected new CollectionFilterMenuItem Item => ((DropdownMenuItem)base.Item).Value; @@ -148,7 +159,7 @@ namespace osu.Game.Collections [Resolved] private IBindable beatmap { get; set; } = null!; - public CollectionDropdownMenuItem(MenuItem item) + public CollectionDropdownDrawableMenuItem(MenuItem item) : base(item) { } diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs index 7594978870..f3f038a7f0 100644 --- a/osu.Game/Collections/DeleteCollectionDialog.cs +++ b/osu.Game/Collections/DeleteCollectionDialog.cs @@ -4,16 +4,17 @@ 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 collection, Action deleteAction) { HeaderText = "Confirm deletion of"; - BodyText = $"{collection.Name} ({"beatmap".ToQuantity(collection.BeatmapMD5Hashes.Count)})"; + BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})"); Icon = FontAwesome.Regular.TrashAlt; diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index f376d18224..1639afd362 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -1,33 +1,51 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.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 { /// /// Visualises a list of s. /// - public class DrawableCollectionList : OsuRearrangeableListContainer + public class DrawableCollectionList : OsuRearrangeableListContainer> { private Scroll scroll = null!; protected override ScrollContainer CreateScrollContainer() => scroll = new Scroll(); - protected override FillFlowContainer> CreateListFillFlowContainer() => new Flow + [Resolved] + private RealmAccess realm { get; set; } = null!; + + protected override FillFlowContainer>> CreateListFillFlowContainer() => new Flow { DragActive = { BindTarget = DragActive } }; - // TODO: source from realm + protected override void LoadComplete() + { + base.LoadComplete(); - protected override OsuRearrangeableListItem CreateOsuDrawable(BeatmapCollection item) + realm.RegisterForNotifications(r => r.All(), collectionsChanged); + } + + private void collectionsChanged(IRealmCollection collections, ChangeSet? changes, Exception error) + { + Items.Clear(); + Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm))); + } + + protected override OsuRearrangeableListItem> CreateOsuDrawable(Live item) { if (item.ID == scroll.PlaceholderItem.Model.ID) return scroll.ReplacePlaceholder(); @@ -97,7 +115,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; } @@ -106,7 +124,7 @@ namespace osu.Game.Collections /// /// The flow of . Disables layout easing unless a drag is in progress. /// - private class Flow : FillFlowContainer> + private class Flow : FillFlowContainer>> { public readonly IBindable DragActive = new Bindable(); diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 6093e69deb..d1e40f6262 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -3,12 +3,12 @@ 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; @@ -23,52 +23,37 @@ namespace osu.Game.Collections /// /// Visualises a inside a . /// - public class DrawableCollectionListItem : OsuRearrangeableListItem + public class DrawableCollectionListItem : OsuRearrangeableListItem> { private const float item_height = 35; private const float button_width = item_height * 0.75f; - /// - /// Whether the currently exists inside realm. - /// - public IBindable IsCreated => isCreated; - - private readonly Bindable isCreated = new Bindable(); - /// /// Creates a new . /// /// The . /// Whether currently exists inside realm. - public DrawableCollectionListItem(BeatmapCollection item, bool isCreated) + public DrawableCollectionListItem(Live 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); /// /// The main content of the . /// private class ItemContent : CircularContainer { - public readonly Bindable IsCreated = new Bindable(); + private readonly Live collection; - private readonly BeatmapCollection collection; - - private Container textBoxPaddingContainer = null!; private ItemTextBox textBox = null!; [Resolved] private RealmAccess realm { get; set; } = null!; - public ItemContent(BeatmapCollection collection) + public ItemContent(Live collection) { this.collection = collection; @@ -80,19 +65,20 @@ namespace osu.Game.Collections [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 @@ -100,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" }, } }, @@ -112,28 +98,18 @@ namespace osu.Game.Collections base.LoadComplete(); // Bind late, as the collection name may change externally while still loading. - textBox.Current.Value = collection.Name; - textBox.Current.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(textBox.Current.Value)) - return; - - // Add the new collection and disable our placeholder. If all text is removed, the placeholder should not show back again. - realm.Write(r => r.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; } } @@ -151,22 +127,17 @@ namespace osu.Game.Collections public class DeleteButton : CompositeDrawable { - public readonly IBindable IsCreated = new Bindable(); - public Func IsTextBoxHovered = null!; [Resolved] private IDialogOverlay? dialogOverlay { get; set; } - [Resolved] - private RealmAccess realmAccess { get; set; } = null!; - - private readonly BeatmapCollection collection; + private readonly Live collection; private Drawable fadeContainer = null!; private Drawable background = null!; - public DeleteButton(BeatmapCollection collection) + public DeleteButton(Live collection) { this.collection = collection; RelativeSizeAxes = Axes.Y; @@ -200,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) @@ -223,7 +188,7 @@ namespace osu.Game.Collections { background.FlashColour(Color4.White, 150); - if (collection.BeatmapMD5Hashes.Count == 0) + if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0) deleteCollection(); else dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection)); @@ -231,7 +196,7 @@ namespace osu.Game.Collections return true; } - private void deleteCollection() => realmAccess.Write(r => r.Remove(collection)); + private void deleteCollection() => collection.PerformWrite(c => c.Realm.Remove(c)); } } } diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 721e0d632e..13737dbd78 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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.Graphics; @@ -23,7 +21,7 @@ namespace osu.Game.Collections private const double enter_duration = 500; private const double exit_duration = 200; - private AudioFilter lowPassFilter; + private AudioFilter lowPassFilter = null!; public ManageCollectionsDialog() { From 1669208a54a232dc2e097bc129b3839209be3b2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 23:19:00 +0900 Subject: [PATCH 08/17] Add migration of existing collections database --- osu.Game/Database/LegacyCollectionImporter.cs | 14 +++++------ osu.Game/Database/LegacyImportManager.cs | 2 +- osu.Game/Database/RealmAccess.cs | 25 ++++++++++++++++++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/osu.Game/Database/LegacyCollectionImporter.cs b/osu.Game/Database/LegacyCollectionImporter.cs index bd32b8c446..4bb28bf731 100644 --- a/osu.Game/Database/LegacyCollectionImporter.cs +++ b/osu.Game/Database/LegacyCollectionImporter.cs @@ -7,8 +7,8 @@ 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; using osu.Game.IO.Legacy; using osu.Game.Overlays.Notifications; @@ -27,14 +27,14 @@ namespace osu.Game.Database this.realm = realm; } - public Task GetAvailableCount(StableStorage stableStorage) + public Task GetAvailableCount(Storage storage) { - if (!stableStorage.Exists(database_name)) + if (!storage.Exists(database_name)) return Task.FromResult(0); return Task.Run(() => { - using (var stream = stableStorage.GetStream(database_name)) + using (var stream = storage.GetStream(database_name)) return readCollections(stream).Count; }); } @@ -42,9 +42,9 @@ namespace osu.Game.Database /// /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. /// - public Task ImportFromStableAsync(StableStorage stableStorage) + public Task ImportFromStorage(Storage storage) { - if (!stableStorage.Exists(database_name)) + 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); @@ -53,7 +53,7 @@ namespace osu.Game.Database return Task.Run(async () => { - using (var stream = stableStorage.GetStream(database_name)) + using (var stream = storage.GetStream(database_name)) await Import(stream).ConfigureAwait(false); }); } diff --git a/osu.Game/Database/LegacyImportManager.cs b/osu.Game/Database/LegacyImportManager.cs index baa117fe07..96f4aaf67c 100644 --- a/osu.Game/Database/LegacyImportManager.cs +++ b/osu.Game/Database/LegacyImportManager.cs @@ -108,7 +108,7 @@ namespace osu.Game.Database importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage)); if (content.HasFlagFast(StableContent.Collections)) - importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess).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)); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index a93fdea35b..6a0d4d34db 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -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). /// - private const int schema_version = 20; + private const int schema_version = 21; /// /// Lock object which is held during 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; } } From 226eefcc5c0a87d26962b4c22b68b2e53211535e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Jul 2022 13:37:01 +0900 Subject: [PATCH 09/17] Add note about hash storage --- osu.Game/Collections/BeatmapCollection.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Collections/BeatmapCollection.cs b/osu.Game/Collections/BeatmapCollection.cs index 2ffe17d9e6..ca5f8dbe53 100644 --- a/osu.Game/Collections/BeatmapCollection.cs +++ b/osu.Game/Collections/BeatmapCollection.cs @@ -26,6 +26,13 @@ namespace osu.Game.Collections /// /// The es of beatmaps contained by the collection. /// + /// + /// We store as hashes rather than references to 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. + /// public IList BeatmapMD5Hashes { get; } = null!; /// From ad482b8afcb23dedba8939b0ebaf7c960c4f5c25 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Jul 2022 13:48:15 +0900 Subject: [PATCH 10/17] Tidy up naming of collection dropdowns --- .../TestSceneManageCollectionsDialog.cs | 2 +- .../Navigation/TestSceneScreenNavigation.cs | 4 +-- .../SongSelect/TestSceneFilterControl.cs | 8 ++--- ...ilterDropdown.cs => CollectionDropdown.cs} | 33 +++++++++++-------- osu.Game/Overlays/Music/FilterControl.cs | 4 +-- ...own.cs => NowPlayingCollectionDropdown.cs} | 4 +-- osu.Game/Screens/Select/FilterControl.cs | 4 +-- 7 files changed, 33 insertions(+), 26 deletions(-) rename osu.Game/Collections/{CollectionFilterDropdown.cs => CollectionDropdown.cs} (89%) rename osu.Game/Overlays/Music/{CollectionDropdown.cs => NowPlayingCollectionDropdown.cs} (93%) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index afcb511a6a..6a88ce1ba6 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -149,7 +149,7 @@ namespace osu.Game.Tests.Visual.Collections { AddStep("add dropdown", () => { - Add(new CollectionFilterDropdown + Add(new CollectionDropdown { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index a61352f954..58898d8386 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -74,14 +74,14 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("set filter again", () => songSelect.ChildrenOfType().Single().Current.Value = "test"); AddStep("open collections dropdown", () => { - InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single()); + InputManager.MoveMouseTo(songSelect.ChildrenOfType().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().Single() + .ChildrenOfType().Single() .ChildrenOfType.DropdownMenu>().Single().State == MenuState.Closed); AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1)); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index feb71def5d..a07bfaee2a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1")))); AddStep("select collection", () => { - var dropdown = control.ChildrenOfType().Single(); + var dropdown = control.ChildrenOfType().Single(); dropdown.Current.Value = dropdown.ItemSource.ElementAt(1); }); @@ -210,7 +210,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) => AddAssert($"collection dropdown header displays '{collectionName}'", - () => shouldDisplay == (control.ChildrenOfType().Single().ChildrenOfType().First().Text == collectionName)); + () => shouldDisplay == (control.ChildrenOfType().Single().ChildrenOfType().First().Text == collectionName)); private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", @@ -222,7 +222,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void addExpandHeaderStep() => AddStep("expand header", () => { - InputManager.MoveMouseTo(control.ChildrenOfType().Single()); + InputManager.MoveMouseTo(control.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); @@ -233,6 +233,6 @@ namespace osu.Game.Tests.Visual.SongSelect }); private IEnumerable.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems() - => control.ChildrenOfType().Single().ChildrenOfType.DropdownMenu.DrawableDropdownMenuItem>(); + => control.ChildrenOfType().Single().ChildrenOfType.DropdownMenu.DrawableDropdownMenuItem>(); } } diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionDropdown.cs similarity index 89% rename from osu.Game/Collections/CollectionFilterDropdown.cs rename to osu.Game/Collections/CollectionDropdown.cs index 790a23d2e5..197e0d1837 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionDropdown.cs @@ -21,9 +21,9 @@ using Realms; namespace osu.Game.Collections { /// - /// A dropdown to select the to filter beatmaps using. + /// A dropdown to select the collection to be used to filter results. /// - public class CollectionFilterDropdown : OsuDropdown + public class CollectionDropdown : OsuDropdown { /// /// Whether to show the "manage collections..." menu item in the dropdown. @@ -40,7 +40,9 @@ namespace osu.Game.Collections [Resolved] private RealmAccess realm { get; set; } = null!; - public CollectionFilterDropdown() + private IDisposable? realmSubscription; + + public CollectionDropdown() { ItemSource = filters; @@ -51,7 +53,7 @@ namespace osu.Game.Collections { base.LoadComplete(); - realm.RegisterForNotifications(r => r.All(), collectionsChanged); + realmSubscription = realm.RegisterForNotifications(r => r.All(), collectionsChanged); Current.BindValueChanged(selectionChanged); } @@ -114,6 +116,12 @@ namespace osu.Game.Collections } } + 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(); @@ -150,18 +158,19 @@ namespace osu.Game.Collections protected class CollectionDropdownDrawableMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem { - protected new CollectionFilterMenuItem Item => ((DropdownMenuItem)base.Item).Value; - private IconButton addOrRemoveButton = null!; private bool beatmapInCollection; + private readonly Live? collection; + [Resolved] private IBindable beatmap { get; set; } = null!; public CollectionDropdownDrawableMenuItem(MenuItem item) : base(item) { + collection = ((DropdownMenuItem)item).Value.Collection; } [BackgroundDependencyLoader] @@ -181,13 +190,11 @@ namespace osu.Game.Collections { base.LoadComplete(); - if (Item.Collection != null) + if (collection != null) { beatmap.BindValueChanged(_ => { - Debug.Assert(Item.Collection != null); - - beatmapInCollection = Item.Collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.Value.BeatmapInfo.MD5Hash)); + 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; @@ -220,7 +227,7 @@ namespace osu.Game.Collections private void updateButtonVisibility() { - if (Item.Collection == null) + if (collection == null) addOrRemoveButton.Alpha = 0; else addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0; @@ -228,9 +235,9 @@ namespace osu.Game.Collections private void addOrRemove() { - Debug.Assert(Item.Collection != null); + Debug.Assert(collection != null); - Item.Collection.PerformWrite(c => + collection.PerformWrite(c => { if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash)) c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash); diff --git a/osu.Game/Overlays/Music/FilterControl.cs b/osu.Game/Overlays/Music/FilterControl.cs index eb12a62864..ffa50c3a35 100644 --- a/osu.Game/Overlays/Music/FilterControl.cs +++ b/osu.Game/Overlays/Music/FilterControl.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Music public Action 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 } }, }, }; diff --git a/osu.Game/Overlays/Music/CollectionDropdown.cs b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs similarity index 93% rename from osu.Game/Overlays/Music/CollectionDropdown.cs rename to osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs index c1ba16788e..635a2e5044 100644 --- a/osu.Game/Overlays/Music/CollectionDropdown.cs +++ b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs @@ -15,9 +15,9 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Music { /// - /// A for use in the . + /// A for use in the . /// - public class CollectionDropdown : CollectionFilterDropdown + public class NowPlayingCollectionDropdown : CollectionDropdown { protected override bool ShowManageCollectionsItem => false; diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index f07817d5dc..ae82285fba 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Select private SeekLimitedSearchTextBox searchTextBox; - private CollectionFilterDropdown collectionDropdown; + private CollectionDropdown collectionDropdown; public FilterCriteria CreateCriteria() { @@ -179,7 +179,7 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.Both, Width = 0.48f, }, - collectionDropdown = new CollectionFilterDropdown + collectionDropdown = new CollectionDropdown { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, From da0646789120f33153946618d0ed2e414171bb17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Jul 2022 13:50:19 +0900 Subject: [PATCH 11/17] Add missing realm subscription cleanup --- osu.Game/Collections/DrawableCollectionList.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 1639afd362..8546ba53c2 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -20,13 +20,15 @@ namespace osu.Game.Collections /// public class DrawableCollectionList : OsuRearrangeableListContainer> { - private Scroll scroll = null!; - protected override ScrollContainer CreateScrollContainer() => scroll = new Scroll(); [Resolved] private RealmAccess realm { get; set; } = null!; + private Scroll scroll = null!; + + private IDisposable? realmSubscription; + protected override FillFlowContainer>> CreateListFillFlowContainer() => new Flow { DragActive = { BindTarget = DragActive } @@ -36,7 +38,7 @@ namespace osu.Game.Collections { base.LoadComplete(); - realm.RegisterForNotifications(r => r.All(), collectionsChanged); + realmSubscription = realm.RegisterForNotifications(r => r.All(), collectionsChanged); } private void collectionsChanged(IRealmCollection collections, ChangeSet? changes, Exception error) @@ -53,6 +55,12 @@ namespace osu.Game.Collections return new DrawableCollectionListItem(item, true); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + realmSubscription?.Dispose(); + } + /// /// The scroll container for this . /// Contains the main flow of and attaches a placeholder item to the end of the list. From 392cb352cc71da8b0f82aa0877ea6a7febfc54b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Jul 2022 14:07:42 +0900 Subject: [PATCH 12/17] Force alphabetical ordering for now --- osu.Game/Collections/CollectionDropdown.cs | 2 +- osu.Game/Collections/DrawableCollectionList.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Collections/CollectionDropdown.cs b/osu.Game/Collections/CollectionDropdown.cs index 197e0d1837..43a4d90aa8 100644 --- a/osu.Game/Collections/CollectionDropdown.cs +++ b/osu.Game/Collections/CollectionDropdown.cs @@ -53,7 +53,7 @@ namespace osu.Game.Collections { base.LoadComplete(); - realmSubscription = realm.RegisterForNotifications(r => r.All(), collectionsChanged); + realmSubscription = realm.RegisterForNotifications(r => r.All().OrderBy(c => c.Name), collectionsChanged); Current.BindValueChanged(selectionChanged); } diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 8546ba53c2..0f4362fff3 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -38,7 +38,7 @@ namespace osu.Game.Collections { base.LoadComplete(); - realmSubscription = realm.RegisterForNotifications(r => r.All(), collectionsChanged); + realmSubscription = realm.RegisterForNotifications(r => r.All().OrderBy(c => c.Name), collectionsChanged); } private void collectionsChanged(IRealmCollection collections, ChangeSet? changes, Exception error) From ca6857447345d4dc35aadeba1241f91b63dcc645 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Jul 2022 14:35:27 +0900 Subject: [PATCH 13/17] Make `NotificationOverlay` dependency optional in `CollectionSettings` --- .../Settings/Sections/Maintenance/CollectionsSettings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs index 498859db46..5a91213eb8 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private RealmAccess realm { get; set; } = null!; [Resolved] - private INotificationOverlay notificationOverlay { get; set; } = null!; + private INotificationOverlay? notificationOverlay { get; set; } [BackgroundDependencyLoader] private void load(LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay) @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private void deleteAllCollections() { realm.Write(r => r.RemoveAll()); - notificationOverlay.Post(new ProgressCompletionNotification { Text = "Deleted all collections!" }); + notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all collections!" }); } } } From 8ac886a247c9cb1d25d8eba6c8048b6654561d2f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Jul 2022 15:20:25 +0900 Subject: [PATCH 14/17] Update test to account for sort order --- .../Visual/Collections/TestSceneManageCollectionsDialog.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 6a88ce1ba6..13393254a6 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -253,9 +253,14 @@ namespace osu.Game.Tests.Visual.Collections }); }); + assertCollectionName(0, "1"); + assertCollectionName(1, "1"); + AddStep("change first collection name", () => Realm.Write(_ => first.Name = "First")); - assertCollectionName(0, "First"); + // Item will have moved due to alphabetical sorting. + assertCollectionName(0, "2"); + assertCollectionName(1, "First"); } [Test] From 6bf293e130b98c35f7c896d8f5e174b0b436f04b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Jul 2022 15:45:32 +0900 Subject: [PATCH 15/17] Fix managed object reused between test runs --- osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index a07bfaee2a..d3b3238bb0 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -78,9 +78,9 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestCollectionRemovedFromDropdown() { - var first = new BeatmapCollection(name: "1"); + BeatmapCollection first = null!; - AddStep("add collection", () => Realm.Write(r => r.Add(first))); + 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))); From 525e4a20193a8f5d644595fecddea0f9ef516f97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Jul 2022 15:51:18 +0900 Subject: [PATCH 16/17] Fix crash in `DrawableRoomPlaylistItem` context menu creation due to incorrect enumeration casting --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 492bb8ada5..8dccc3d82f 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -499,7 +499,7 @@ namespace osu.Game.Screens.OnlinePlay { if (beatmaps.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID) is BeatmapInfo local && !local.BeatmapSet.AsNonNull().DeletePending) { - var collectionItems = realm.Realm.All().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast().ToList(); + var collectionItems = realm.Realm.All().AsEnumerable().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast().ToList(); if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); From cd01c5d3acefc5678be6dd6f7e3653acdbce8fc7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Jul 2022 16:34:31 +0900 Subject: [PATCH 17/17] Fix assertion --- .../Visual/Collections/TestSceneManageCollectionsDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 13393254a6..4c89fc1ab9 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -254,7 +254,7 @@ namespace osu.Game.Tests.Visual.Collections }); assertCollectionName(0, "1"); - assertCollectionName(1, "1"); + assertCollectionName(1, "2"); AddStep("change first collection name", () => Realm.Write(_ => first.Name = "First"));