diff --git a/src/NexusMods.App.UI/Extensions/ObservableExtensions.cs b/src/NexusMods.App.UI/Extensions/ObservableExtensions.cs index 15503f2575..4f0c3eff0f 100644 --- a/src/NexusMods.App.UI/Extensions/ObservableExtensions.cs +++ b/src/NexusMods.App.UI/Extensions/ObservableExtensions.cs @@ -1,6 +1,8 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; +using DynamicData; +using DynamicData.Binding; namespace NexusMods.App.UI.Extensions; diff --git a/src/NexusMods.App.UI/Extensions/SourceCacheAdapter.cs b/src/NexusMods.App.UI/Extensions/SourceCacheAdapter.cs new file mode 100644 index 0000000000..d623f53ae4 --- /dev/null +++ b/src/NexusMods.App.UI/Extensions/SourceCacheAdapter.cs @@ -0,0 +1,44 @@ +using DynamicData; + +namespace NexusMods.App.UI.Extensions; + +public class SourceCacheAdapter : IChangeSetAdaptor + where TObject : notnull + where TKey : notnull +{ + private readonly SourceCache _sourceCache; + + public SourceCacheAdapter(SourceCache sourceCache) + { + _sourceCache = sourceCache; + } + + public void Adapt(IChangeSet changes) + { + _sourceCache.Edit(updater => + { + foreach (var change in changes) + { + switch (change.Reason) + { + case ChangeReason.Add: + updater.AddOrUpdate(change.Current, change.Key); + break; + case ChangeReason.Update: + updater.AddOrUpdate(change.Current, change.Key); + break; + case ChangeReason.Remove: + updater.RemoveKey(change.Key); + break; + case ChangeReason.Refresh: + updater.Refresh(change.Key); + break; + case ChangeReason.Moved: + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + }); + } +} diff --git a/src/NexusMods.App.UI/NexusMods.App.UI.csproj b/src/NexusMods.App.UI/NexusMods.App.UI.csproj index 744f7ef406..f7ec2ae75b 100644 --- a/src/NexusMods.App.UI/NexusMods.App.UI.csproj +++ b/src/NexusMods.App.UI/NexusMods.App.UI.csproj @@ -666,7 +666,4 @@ Language.Designer.cs - - - diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/FakeParentLibraryItemModel.cs b/src/NexusMods.App.UI/Pages/LibraryPage/FakeParentLibraryItemModel.cs index b98334f955..fdfcbed5b5 100644 --- a/src/NexusMods.App.UI/Pages/LibraryPage/FakeParentLibraryItemModel.cs +++ b/src/NexusMods.App.UI/Pages/LibraryPage/FakeParentLibraryItemModel.cs @@ -17,8 +17,19 @@ public class FakeParentLibraryItemModel : LibraryItemModel public override IReadOnlyCollection GetLoadoutItemIds() => LibraryItems.Select(static item => item.LibraryItemId).ToArray(); private readonly IDisposable _modelActivationDisposable; + private readonly IDisposable _activationSelectionDisposable; + public FakeParentLibraryItemModel(LibraryItemId libraryItemId) : base(libraryItemId) { + _activationSelectionDisposable = Activation.CombineLatest(IsSelected, (a, b) => (a, b)).Subscribe(this, static (tuple, self) => + { + var (isActivating, isSelected) = tuple; + if (!isActivating && !isSelected) + { + self.LibraryItems.Clear(); + } + }); + _modelActivationDisposable = WhenModelActivated(this, static (model, disposables) => { model.NumInstalledObservable @@ -54,7 +65,6 @@ public FakeParentLibraryItemModel(LibraryItemId libraryItemId) : base(libraryIte .AddTo(disposables); model.LibraryItemsObservable.OnUI().SubscribeWithErrorLogging(changeSet => model.LibraryItems.ApplyChanges(changeSet)).AddTo(disposables); - Disposable.Create(model.LibraryItems, static libraryFiles => libraryFiles.Clear()).AddTo(disposables); }); } @@ -65,7 +75,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - _modelActivationDisposable.Dispose(); + Disposable.Dispose(_modelActivationDisposable, _activationSelectionDisposable); } LibraryItems = null!; diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/LibraryItemModel.cs b/src/NexusMods.App.UI/Pages/LibraryPage/LibraryItemModel.cs index fa49b8510e..80f36666a8 100644 --- a/src/NexusMods.App.UI/Pages/LibraryPage/LibraryItemModel.cs +++ b/src/NexusMods.App.UI/Pages/LibraryPage/LibraryItemModel.cs @@ -43,6 +43,8 @@ public class LibraryItemModel : TreeDataGridItemModel GetLoadoutItemIds() => _fixedId; private readonly IDisposable _modelActivationDisposable; + private readonly IDisposable _activationSelectionDisposable; + public LibraryItemModel(LibraryItemId libraryItemId) { _fixedId = [libraryItemId]; @@ -50,6 +52,15 @@ public LibraryItemModel(LibraryItemId libraryItemId) var canInstall = IsInstalledInLoadout.Select(static b => !b); InstallCommand = canInstall.ToReactiveCommand>(_ => GetLoadoutItemIds(), initialCanExecute: false); + _activationSelectionDisposable = Activation.CombineLatest(IsSelected, (a, b) => (a, b)).Subscribe(this, static (tuple, self) => + { + var (isActivating, isSelected) = tuple; + if (!isActivating && !isSelected) + { + self.LinkedLoadoutItems.Clear(); + } + }); + _modelActivationDisposable = WhenModelActivated(this, static (model, disposables) => { Debug.Assert(model.Ticker is not null, "should've been set before activation"); @@ -83,7 +94,6 @@ public LibraryItemModel(LibraryItemId libraryItemId) model.FormattedInstalledDate.Value = FormatDate(DateTime.Now, model.InstalledDate.Value); model.LinkedLoadoutItemsObservable.OnUI().SubscribeWithErrorLogging(changeSet => model.LinkedLoadoutItems.ApplyChanges(changeSet)).AddTo(disposables); - Disposable.Create(model.LinkedLoadoutItems, static items => items.Clear()).AddTo(disposables); }); } @@ -103,6 +113,7 @@ protected override void Dispose(bool disposing) Disposable.Dispose( InstallCommand, _modelActivationDisposable, + _activationSelectionDisposable, FormattedCreatedAtDate, FormattedInstalledDate, ItemSize, diff --git a/src/NexusMods.App.UI/Pages/LoadoutPage/FakeParentLoadoutItemModel.cs b/src/NexusMods.App.UI/Pages/LoadoutPage/FakeParentLoadoutItemModel.cs index c956602d53..8ded97a193 100644 --- a/src/NexusMods.App.UI/Pages/LoadoutPage/FakeParentLoadoutItemModel.cs +++ b/src/NexusMods.App.UI/Pages/LoadoutPage/FakeParentLoadoutItemModel.cs @@ -17,14 +17,23 @@ public class FakeParentLoadoutItemModel : LoadoutItemModel public override IReadOnlyCollection GetLoadoutItemIds() => LoadoutItemIds; private readonly IDisposable _modelActivationDisposable; + private readonly IDisposable _activationSelectionDisposable; + public FakeParentLoadoutItemModel() : base(default(LoadoutItemId)) { + _activationSelectionDisposable = Activation.CombineLatest(IsSelected, (a, b) => (a, b)).Subscribe(this, static (tuple, self) => + { + var (isActivating, isSelected) = tuple; + if (!isActivating && !isSelected) + { + self.LoadoutItemIds.Clear(); + } + }); + _modelActivationDisposable = WhenModelActivated(this, static (model, disposables) => { model.InstalledAtObservable.OnUI().Subscribe(date => model.InstalledAt.Value = date).AddTo(disposables); - model.LoadoutItemIdsObservable.OnUI().SubscribeWithErrorLogging(changeSet => model.LoadoutItemIds.ApplyChanges(changeSet)).AddTo(disposables); - Disposable.Create(model.LoadoutItemIds, static collection => collection.Clear()).AddTo(disposables); }); } @@ -35,7 +44,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - _modelActivationDisposable.Dispose(); + Disposable.Dispose(_modelActivationDisposable, _activationSelectionDisposable); } LoadoutItemIds = null!; diff --git a/src/NexusMods.App.UI/Pages/LocalFileDataProvider.cs b/src/NexusMods.App.UI/Pages/LocalFileDataProvider.cs index 3b71287d74..0bf0f3cef5 100644 --- a/src/NexusMods.App.UI/Pages/LocalFileDataProvider.cs +++ b/src/NexusMods.App.UI/Pages/LocalFileDataProvider.cs @@ -115,29 +115,19 @@ public IObservable> ObserveNestedLoadoutI { var libraryFile = LibraryFile.Load(_connection.Db, entityId); - var observable = _connection + // TODO: dispose + var cache = new SourceCache(static item => item.Id); + var disposable = _connection .ObserveDatoms(LibraryLinkedLoadoutItem.LibraryItemId, entityId) .AsEntityIds() .FilterInStaticLoadout(_connection, loadoutFilter) .Transform((_, e) => LibraryLinkedLoadoutItem.Load(_connection.Db, e)) - .PublishWithFunc(() => - { - var changeSet = new ChangeSet(); - var entities = LibraryLinkedLoadoutItem.FindByLibraryItem(_connection.Db, libraryFile.Id); - - foreach (var entity in entities) - { - if (!entity.AsLoadoutItemGroup().AsLoadoutItem().LoadoutId.Equals(loadoutFilter.LoadoutId)) continue; - changeSet.Add(new Change(ChangeReason.Add, entity.Id, entity)); - } - - return changeSet; - }) - .AutoConnect(); + .Adapt(new SourceCacheAdapter(cache)) + .SubscribeWithErrorLogging(); - var childrenObservable = observable.Transform(libraryLinkedLoadoutItem => LoadoutDataProviderHelper.ToLoadoutItemModel(_connection, libraryLinkedLoadoutItem)); + var childrenObservable = cache.Connect().Transform(libraryLinkedLoadoutItem => LoadoutDataProviderHelper.ToLoadoutItemModel(_connection, libraryLinkedLoadoutItem)); - var installedAtObservable = observable + var installedAtObservable = cache.Connect() .Transform(item => item.GetCreatedAt()) .QueryWhenChanged(query => { @@ -145,9 +135,9 @@ public IObservable> ObserveNestedLoadoutI return query.Items.Max(); }); - var loadoutItemIdsObservable = observable.Transform(item => item.AsLoadoutItemGroup().AsLoadoutItem().LoadoutItemId); + var loadoutItemIdsObservable = cache.Connect().Transform(item => item.AsLoadoutItemGroup().AsLoadoutItem().LoadoutItemId); - var isEnabledObservable = observable + var isEnabledObservable = cache.Connect() .TransformOnObservable(x => LoadoutItem.Observe(_connection, x.Id).Select(item => !item.IsDisabled)) .QueryWhenChanged(query => { diff --git a/src/NexusMods.App.UI/Pages/NexusModsDataProvider.cs b/src/NexusMods.App.UI/Pages/NexusModsDataProvider.cs index 8ab6ebe220..bf499d02a5 100644 --- a/src/NexusMods.App.UI/Pages/NexusModsDataProvider.cs +++ b/src/NexusMods.App.UI/Pages/NexusModsDataProvider.cs @@ -72,39 +72,31 @@ private LibraryItemModel ToLibraryItemModel(NexusModsLibraryFile.ReadOnly nexusM private LibraryItemModel ToLibraryItemModel(NexusModsModPageMetadata.ReadOnly modPageMetadata, LibraryFilter libraryFilter) { - var nexusModsLibraryFileObservable = _connection + // TODO: dispose + var cache = new SourceCache(static datom => datom.E); + var disposable = _connection .ObserveDatoms(NexusModsLibraryFile.ModPageMetadataId, modPageMetadata.Id) .AsEntityIds() - .PublishWithFunc(initialValueFunc: () => - { - var changeSet = new ChangeSet(); - var datoms = _connection.Db.Datoms(NexusModsLibraryFile.ModPageMetadataId, modPageMetadata.Id); - foreach (var datom in datoms) - { - changeSet.Add(new Change(ChangeReason.Add, datom.E, datom)); - } - - return changeSet; - }) - .AutoConnect(); + .Adapt(new SourceCacheAdapter(cache)) + .SubscribeWithErrorLogging(); - var hasChildrenObservable = nexusModsLibraryFileObservable.IsNotEmpty(); - var childrenObservable = nexusModsLibraryFileObservable.Transform((_, e) => + var hasChildrenObservable = cache.Connect().IsNotEmpty(); + var childrenObservable = cache.Connect().Transform((_, e) => { var libraryFile = NexusModsLibraryFile.Load(_connection.Db, e); return ToLibraryItemModel(libraryFile, libraryFilter); }); - var linkedLoadoutItemsObservable = nexusModsLibraryFileObservable + var linkedLoadoutItemsObservable = cache.Connect() // NOTE(erri120): DynamicData 9.0.4 is broken for value types because it uses ReferenceEquals. Temporary workaround is a custom equality comparer. .MergeManyChangeSets((_, e) => _connection.ObserveDatoms(LibraryLinkedLoadoutItem.LibraryItemId, e).AsEntityIds(), equalityComparer: DatomEntityIdEqualityComparer.Instance) .FilterInObservableLoadout(_connection, libraryFilter) .Transform((_, e) => LibraryLinkedLoadoutItem.Load(_connection.Db, e)); - var libraryFilesObservable = nexusModsLibraryFileObservable + var libraryFilesObservable = cache.Connect() .Transform((_, e) => NexusModsLibraryFile.Load(_connection.Db, e).AsDownloadedFile().AsLibraryFile().AsLibraryItem()); - var numInstalledObservable = nexusModsLibraryFileObservable.TransformOnObservable((_, e) => _connection + var numInstalledObservable = cache.Connect().TransformOnObservable((_, e) => _connection .ObserveDatoms(LibraryLinkedLoadoutItem.LibraryItemId, e) .AsEntityIds() .FilterInObservableLoadout(_connection, libraryFilter) @@ -141,39 +133,25 @@ public IObservable> ObserveNestedLoadoutI ) .Transform(modPage => { - var observable = _connection + // TODO: dispose + var cache = new SourceCache(static datom => datom.E); + var disposable = _connection .ObserveDatoms(NexusModsLibraryFile.ModPageMetadataId, modPage.Id).AsEntityIds() .FilterOnObservable((_, e) => _connection.ObserveDatoms(LibraryLinkedLoadoutItem.LibraryItemId, e).IsNotEmpty()) // NOTE(erri120): DynamicData 9.0.4 is broken for value types because it uses ReferenceEquals. Temporary workaround is a custom equality comparer. .MergeManyChangeSets((_, e) => _connection.ObserveDatoms(LibraryLinkedLoadoutItem.LibraryItemId, e).AsEntityIds(), equalityComparer: DatomEntityIdEqualityComparer.Instance) .FilterInStaticLoadout(_connection, loadoutFilter) - .PublishWithFunc(() => - { - var changeSet = new ChangeSet(); - - var libraryFileDatoms = _connection.Db.Datoms(NexusModsLibraryFile.ModPageMetadataId, modPage.Id); - foreach (var entityIdDatom in libraryFileDatoms) - { - var libraryLinkedLoadoutItemDatoms = _connection.Db.Datoms(LibraryLinkedLoadoutItem.LibraryItemId, entityIdDatom.E); - foreach (var datom in libraryLinkedLoadoutItemDatoms) - { - if (!LoadoutItem.Load(_connection.Db, datom.E).LoadoutId.Equals(loadoutFilter.LoadoutId)) continue; - changeSet.Add(new Change(ChangeReason.Add, datom.E, datom)); - } - } - - return changeSet; - }) - .AutoConnect(); + .Adapt(new SourceCacheAdapter(cache)) + .SubscribeWithErrorLogging(); - var hasChildrenObservable = observable.IsNotEmpty(); - var childrenObservable = observable.Transform(libraryLinkedLoadoutItemDatom => + var hasChildrenObservable = cache.Connect().IsNotEmpty(); + var childrenObservable = cache.Connect().Transform(libraryLinkedLoadoutItemDatom => { var libraryLinkedLoadoutItem = LibraryLinkedLoadoutItem.Load(_connection.Db, libraryLinkedLoadoutItemDatom.E); return LoadoutDataProviderHelper.ToLoadoutItemModel(_connection, libraryLinkedLoadoutItem); }); - var installedAtObservable = observable + var installedAtObservable = cache.Connect() .Transform((_, e) => LibraryLinkedLoadoutItem.Load(_connection.Db, e).GetCreatedAt()) .QueryWhenChanged(query => { @@ -181,9 +159,9 @@ public IObservable> ObserveNestedLoadoutI return query.Items.Max(); }); - var loadoutItemIdsObservable = observable.Transform((_, e) => (LoadoutItemId) e); + var loadoutItemIdsObservable = cache.Connect().Transform((_, e) => (LoadoutItemId) e); - var isEnabledObservable = observable + var isEnabledObservable = cache.Connect() .TransformOnObservable(datom => LoadoutItem.Observe(_connection, datom.E).Select(item => !item.IsDisabled)) .QueryWhenChanged(query => {