diff --git a/src/NexusMods.App.UI/Controls/Trees/LoadoutGroupFileTreeViewModel.cs b/src/NexusMods.App.UI/Controls/Trees/LoadoutItemGroupFileTreeViewModel.cs similarity index 50% rename from src/NexusMods.App.UI/Controls/Trees/LoadoutGroupFileTreeViewModel.cs rename to src/NexusMods.App.UI/Controls/Trees/LoadoutItemGroupFileTreeViewModel.cs index a0edf33cb5..5b816ebf2f 100644 --- a/src/NexusMods.App.UI/Controls/Trees/LoadoutGroupFileTreeViewModel.cs +++ b/src/NexusMods.App.UI/Controls/Trees/LoadoutItemGroupFileTreeViewModel.cs @@ -4,28 +4,34 @@ using System.Runtime.InteropServices; using Avalonia.Controls; using DynamicData; +using DynamicData.Kernel; using Humanizer.Bytes; using NexusMods.Abstractions.GameLocators; using NexusMods.Abstractions.Loadouts; using NexusMods.App.UI.Controls.Trees.Files; using NexusMods.App.UI.Helpers.TreeDataGrid; using NexusMods.App.UI.Resources; +using NexusMods.MnemonicDB.Abstractions; +using NexusMods.MnemonicDB.Abstractions.ElementComparers; +using NexusMods.MnemonicDB.Abstractions.IndexSegments; using NexusMods.Paths; namespace NexusMods.App.UI.Controls.Trees; +using DirectoryData = (Size size, uint numFiles, GamePath path, GamePath parentPath); -public class LoadoutGroupFileTreeViewModel : AViewModel, IFileTreeViewModel + +public class LoadoutItemGroupFileTreeViewModel : AViewModel, IFileTreeViewModel { public ITreeDataGridSource TreeSource { get; } public ReadOnlyObservableCollection StatusBarStrings { get; } - public LoadoutGroupFileTreeViewModel(LoadoutItemGroup.ReadOnly group) + public LoadoutItemGroupFileTreeViewModel(LoadoutItemGroup.ReadOnly group) { var totalNumFiles = 0; var totalSize = Size.Zero; var nodes = new List(); - var directories = new Dictionary(); + var directories = new Dictionary(); foreach (var loadoutItem in group.Children.OfTypeLoadoutItemWithTargetPath()) { @@ -35,14 +41,7 @@ public LoadoutGroupFileTreeViewModel(LoadoutItemGroup.ReadOnly group) size = loadoutFile.Size; } - var node = new FileTreeNodeViewModel( - fullPath: loadoutItem.TargetPath, - parentPath: ((GamePath)loadoutItem.TargetPath).Parent, - isFile: true, - fileSize: size.Value, - numChildFiles: 0, - isDeletion: loadoutItem.IsDeletedFile() - ); + var node = CreateFileNode(loadoutItem, size); nodes.Add(node); @@ -84,29 +83,14 @@ public LoadoutGroupFileTreeViewModel(LoadoutItemGroup.ReadOnly group) foreach (var kv in directories) { var (_, directory) = kv; - var (size, numFiles, path, parentPath) = directory; - if (parentPath.Equals(IFileTreeViewModel.RootParentGamePath)) + if (directory.parentPath.Equals(IFileTreeViewModel.RootParentGamePath)) { - nodes.Add(new FileTreeNodeViewModel( - name: locationsRegister[path.LocationId].ToString(), - fullPath: path, - parentPath: parentPath, - isFile: false, - fileSize: size.Value, - numChildFiles: numFiles - )); + nodes.Add(CreateRootNode(locationsRegister, directory)); } else { - nodes.Add(new FileTreeNodeViewModel( - fullPath: path, - parentPath: parentPath, - isFile: false, - fileSize: size.Value, - numChildFiles: numFiles, - isDeletion: false - )); + nodes.Add(CreateFolderNode(directory)); } } @@ -127,6 +111,85 @@ public LoadoutGroupFileTreeViewModel(LoadoutItemGroup.ReadOnly group) string.Format(Language.ModFileTreeViewModel_StatusBar_Files__0__1, totalNumFiles, ByteSize.FromBytes(totalSize.Value).ToString()), })); } + + /// + /// Returns the appropriate LoadoutItemGroup of files if the selection contains a LoadoutItemGroup containing files, + /// if the selection contains multiple LoadoutItemGroups of files, returns None. + /// + internal static Optional GetViewModFilesLoadoutItemGroup( + IReadOnlyCollection loadoutItemIds, + IConnection connection) + { + var db = connection.Db; + // Only allow when selecting a single item, or an item with a single child + if (loadoutItemIds.Count != 1) return Optional.None; + var currentGroupId = loadoutItemIds.First(); + + var groupDatoms = db.Datoms(LoadoutItemGroup.Group, Null.Instance); + + while (true) + { + var childDatoms = db.Datoms(LoadoutItem.ParentId, currentGroupId); + var childGroups = groupDatoms.MergeByEntityId(childDatoms); + + // We have no child groups, check if children are files + if (childGroups.Count == 0) + { + return LoadoutItemWithTargetPath.TryGet(db, childDatoms[0].E, out _) + ? LoadoutItemGroup.Load(db, currentGroupId) + : Optional.None; + } + + // Single child group, check if that group is valid + if (childGroups.Count == 1) + { + currentGroupId = childGroups.First(); + continue; + } + + // We have multiple child groups, return None + if (childGroups.Count > 1) return Optional.None; + } + } + + private static FileTreeNodeViewModel CreateFolderNode(DirectoryData directory) + { + var (size, numFiles, path, parentPath) = directory; + return new FileTreeNodeViewModel( + fullPath: path, + parentPath: parentPath, + isFile: false, + fileSize: size.Value, + numChildFiles: numFiles, + isDeletion: false + ); + } + + private static FileTreeNodeViewModel CreateRootNode(IGameLocationsRegister locationsRegister, DirectoryData directory) + { + var (size, numFiles, path, parentPath) = directory; + return new FileTreeNodeViewModel( + name: locationsRegister[path.LocationId].ToString(), + fullPath: path, + parentPath: parentPath, + isFile: false, + fileSize: size.Value, + numChildFiles: numFiles + ); + } + + + private static FileTreeNodeViewModel CreateFileNode(LoadoutItemWithTargetPath.ReadOnly loadoutItem, Size size) + { + return new FileTreeNodeViewModel( + fullPath: loadoutItem.TargetPath, + parentPath: ((GamePath)loadoutItem.TargetPath).Parent, + isFile: true, + fileSize: size.Value, + numChildFiles: 0, + isDeletion: loadoutItem.IsDeletedFile() + ); + } internal static HierarchicalTreeDataGridSource CreateTreeSource( ReadOnlyObservableCollection roots) diff --git a/src/NexusMods.App.UI/Controls/Trees/ModFileTreeViewModel.cs b/src/NexusMods.App.UI/Controls/Trees/ModFileTreeViewModel.cs index 32aabce084..919b2b91b7 100644 --- a/src/NexusMods.App.UI/Controls/Trees/ModFileTreeViewModel.cs +++ b/src/NexusMods.App.UI/Controls/Trees/ModFileTreeViewModel.cs @@ -21,7 +21,7 @@ namespace NexusMods.App.UI.Controls.Trees; /// /// This is one implementation of the IFileTreeViewModel, which is used to display a tree of files in a mod. /// -[Obsolete($"To be replaced by {nameof(LoadoutGroupFileTreeViewModel)}")] +[Obsolete($"To be replaced by {nameof(LoadoutItemGroupFileTreeViewModel)}")] public class ModFileTreeViewModel : AViewModel, IFileTreeViewModel { private readonly IConnection _conn; diff --git a/src/NexusMods.App.UI/NexusMods.App.UI.csproj b/src/NexusMods.App.UI/NexusMods.App.UI.csproj index ba3272c76e..84eb2e4732 100644 --- a/src/NexusMods.App.UI/NexusMods.App.UI.csproj +++ b/src/NexusMods.App.UI/NexusMods.App.UI.csproj @@ -583,7 +583,7 @@ ILoadoutBadgeViewModel.cs - + IFileTreeViewModel.cs @@ -622,6 +622,12 @@ ILibraryDataProvider.cs + + IItemContentsFileTreeViewModel.cs + + + ItemContentsFileTreeView.axaml + diff --git a/src/NexusMods.App.UI/Pages/LoadoutGroupFiles/ILoadoutGroupFilesViewModel.cs b/src/NexusMods.App.UI/Pages/ItemContentsFileTree/IItemContentsFileTreeViewModel.cs similarity index 61% rename from src/NexusMods.App.UI/Pages/LoadoutGroupFiles/ILoadoutGroupFilesViewModel.cs rename to src/NexusMods.App.UI/Pages/ItemContentsFileTree/IItemContentsFileTreeViewModel.cs index 0058ba5ce6..c7460d1910 100644 --- a/src/NexusMods.App.UI/Pages/LoadoutGroupFiles/ILoadoutGroupFilesViewModel.cs +++ b/src/NexusMods.App.UI/Pages/ItemContentsFileTree/IItemContentsFileTreeViewModel.cs @@ -4,11 +4,11 @@ using NexusMods.App.UI.WorkspaceSystem; using ReactiveUI; -namespace NexusMods.App.UI.Pages.LoadoutGroupFiles; +namespace NexusMods.App.UI.Pages.ItemContentsFileTree; -public interface ILoadoutGroupFilesViewModel : IPageViewModelInterface +public interface IItemContentsFileTreeViewModel : IPageViewModelInterface { - LoadoutGroupFilesPageContext? Context { get; set; } + ItemContentsFileTreePageContext? Context { get; set; } IFileTreeViewModel? FileTreeViewModel { get; } diff --git a/src/NexusMods.App.UI/Pages/ItemContentsFileTree/ItemContentsFileTreePage.cs b/src/NexusMods.App.UI/Pages/ItemContentsFileTree/ItemContentsFileTreePage.cs new file mode 100644 index 0000000000..5678449061 --- /dev/null +++ b/src/NexusMods.App.UI/Pages/ItemContentsFileTree/ItemContentsFileTreePage.cs @@ -0,0 +1,29 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using NexusMods.Abstractions.Loadouts; +using NexusMods.Abstractions.Serialization.Attributes; +using NexusMods.App.UI.WorkspaceSystem; + +namespace NexusMods.App.UI.Pages.ItemContentsFileTree; + +[JsonName("NexusMods.App.UI.Pages.ItemContentsFileTreePageContext")] +public record ItemContentsFileTreePageContext : IPageFactoryContext +{ + public required LoadoutItemGroupId GroupId { get; init; } +} + +[UsedImplicitly] +public class ItemContentsFileTreePageFactory : APageFactory +{ + public ItemContentsFileTreePageFactory(IServiceProvider serviceProvider) : base(serviceProvider) { } + + public static readonly PageFactoryId StaticId = PageFactoryId.From(Guid.Parse("85d19521-0414-4a31-a873-43757234ed43")); + public override PageFactoryId Id => StaticId; + + public override IItemContentsFileTreeViewModel CreateViewModel(ItemContentsFileTreePageContext context) + { + var vm = ServiceProvider.GetRequiredService(); + vm.Context = context; + return vm; + } +} diff --git a/src/NexusMods.App.UI/Pages/LoadoutGroupFiles/LoadoutGroupFilesView.axaml b/src/NexusMods.App.UI/Pages/ItemContentsFileTree/ItemContentsFileTreeView.axaml similarity index 82% rename from src/NexusMods.App.UI/Pages/LoadoutGroupFiles/LoadoutGroupFilesView.axaml rename to src/NexusMods.App.UI/Pages/ItemContentsFileTree/ItemContentsFileTreeView.axaml index 466b5b4328..e802e6c6b8 100644 --- a/src/NexusMods.App.UI/Pages/LoadoutGroupFiles/LoadoutGroupFilesView.axaml +++ b/src/NexusMods.App.UI/Pages/ItemContentsFileTree/ItemContentsFileTreeView.axaml @@ -1,5 +1,5 @@ + x:Class="NexusMods.App.UI.Pages.ItemContentsFileTree.ItemContentsFileTreeView"> diff --git a/src/NexusMods.App.UI/Pages/LoadoutGroupFiles/LoadoutGroupFilesView.axaml.cs b/src/NexusMods.App.UI/Pages/ItemContentsFileTree/ItemContentsFileTreeView.cs similarity index 71% rename from src/NexusMods.App.UI/Pages/LoadoutGroupFiles/LoadoutGroupFilesView.axaml.cs rename to src/NexusMods.App.UI/Pages/ItemContentsFileTree/ItemContentsFileTreeView.cs index eda99399c2..e3eee3aa9d 100644 --- a/src/NexusMods.App.UI/Pages/LoadoutGroupFiles/LoadoutGroupFilesView.axaml.cs +++ b/src/NexusMods.App.UI/Pages/ItemContentsFileTree/ItemContentsFileTreeView.cs @@ -2,11 +2,11 @@ using Avalonia.ReactiveUI; using ReactiveUI; -namespace NexusMods.App.UI.Pages.LoadoutGroupFiles; +namespace NexusMods.App.UI.Pages.ItemContentsFileTree; -public partial class LoadoutGroupFilesView : ReactiveUserControl +public partial class ItemContentsFileTreeView : ReactiveUserControl { - public LoadoutGroupFilesView() + public ItemContentsFileTreeView() { InitializeComponent(); diff --git a/src/NexusMods.App.UI/Pages/LoadoutGroupFiles/LoadoutGroupFilesViewModel.cs b/src/NexusMods.App.UI/Pages/ItemContentsFileTree/ItemContentsFileTreeViewModel.cs similarity index 88% rename from src/NexusMods.App.UI/Pages/LoadoutGroupFiles/LoadoutGroupFilesViewModel.cs rename to src/NexusMods.App.UI/Pages/ItemContentsFileTree/ItemContentsFileTreeViewModel.cs index b4f7b9925a..891988ece6 100644 --- a/src/NexusMods.App.UI/Pages/LoadoutGroupFiles/LoadoutGroupFilesViewModel.cs +++ b/src/NexusMods.App.UI/Pages/ItemContentsFileTree/ItemContentsFileTreeViewModel.cs @@ -16,19 +16,19 @@ using ReactiveUI; using ReactiveUI.Fody.Helpers; -namespace NexusMods.App.UI.Pages.LoadoutGroupFiles; +namespace NexusMods.App.UI.Pages.ItemContentsFileTree; [UsedImplicitly] -public class LoadoutGroupFilesViewModel : APageViewModel, ILoadoutGroupFilesViewModel +public class ItemContentsFileTreeViewModel : APageViewModel, IItemContentsFileTreeViewModel { - [Reactive] public LoadoutGroupFilesPageContext? Context { get; set; } + [Reactive] public ItemContentsFileTreePageContext? Context { get; set; } [Reactive] public IFileTreeViewModel? FileTreeViewModel { get; [UsedImplicitly] private set; } [Reactive] public FileTreeNodeViewModel? SelectedItem { get; [UsedImplicitly] private set; } public ReactiveCommand OpenEditorCommand { get; } - public LoadoutGroupFilesViewModel( - ILogger logger, + public ItemContentsFileTreeViewModel( + ILogger logger, IWindowManager windowManager, IConnection connection) : base(windowManager) { @@ -71,7 +71,7 @@ public LoadoutGroupFilesViewModel( .WhereNotNull() .Select(context => LoadoutItemGroup.Load(connection.Db, context.GroupId)) .Where(group => group.IsValid()) - .Select(group => new LoadoutGroupFileTreeViewModel(group)) + .Select(group => new LoadoutItemGroupFileTreeViewModel(group)) .BindToVM(this, vm => vm.FileTreeViewModel) .DisposeWith(disposables); diff --git a/src/NexusMods.App.UI/Pages/LoadoutGrid/LoadoutGridViewModel.cs b/src/NexusMods.App.UI/Pages/LoadoutGrid/LoadoutGridViewModel.cs index 5168bc641d..17aa3917b8 100644 --- a/src/NexusMods.App.UI/Pages/LoadoutGrid/LoadoutGridViewModel.cs +++ b/src/NexusMods.App.UI/Pages/LoadoutGrid/LoadoutGridViewModel.cs @@ -11,10 +11,10 @@ using NexusMods.Abstractions.Settings; using NexusMods.App.UI.Controls.DataGrid; using NexusMods.App.UI.Controls.Navigation; +using NexusMods.App.UI.Pages.ItemContentsFileTree; using NexusMods.App.UI.Pages.LoadoutGrid.Columns.ModEnabled; using NexusMods.App.UI.Pages.LoadoutGrid.Columns.ModInstalled; using NexusMods.App.UI.Pages.LoadoutGrid.Columns.ModName; -using NexusMods.App.UI.Pages.LoadoutGroupFiles; using NexusMods.App.UI.Pages.ModLibrary; using NexusMods.App.UI.Resources; using NexusMods.App.UI.Windows; @@ -107,8 +107,8 @@ public LoadoutGridViewModel( var pageData = new PageData { - FactoryId = LoadoutGroupFilesPageFactory.StaticId, - Context = new LoadoutGroupFilesPageContext + FactoryId = ItemContentsFileTreePageFactory.StaticId, + Context = new ItemContentsFileTreePageContext { GroupId = groupId, }, diff --git a/src/NexusMods.App.UI/Pages/LoadoutGroupFiles/LoadoutGroupFilesPage.cs b/src/NexusMods.App.UI/Pages/LoadoutGroupFiles/LoadoutGroupFilesPage.cs deleted file mode 100644 index 30cfa3c760..0000000000 --- a/src/NexusMods.App.UI/Pages/LoadoutGroupFiles/LoadoutGroupFilesPage.cs +++ /dev/null @@ -1,29 +0,0 @@ -using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; -using NexusMods.Abstractions.Loadouts; -using NexusMods.Abstractions.Serialization.Attributes; -using NexusMods.App.UI.WorkspaceSystem; - -namespace NexusMods.App.UI.Pages.LoadoutGroupFiles; - -[JsonName("NexusMods.App.UI.Pages.LoadoutGroupFilesPageContext")] -public record LoadoutGroupFilesPageContext : IPageFactoryContext -{ - public required LoadoutItemGroupId GroupId { get; init; } -} - -[UsedImplicitly] -public class LoadoutGroupFilesPageFactory : APageFactory -{ - public LoadoutGroupFilesPageFactory(IServiceProvider serviceProvider) : base(serviceProvider) { } - - public static readonly PageFactoryId StaticId = PageFactoryId.From(Guid.Parse("85d19521-0414-4a31-a873-43757234ed43")); - public override PageFactoryId Id => StaticId; - - public override ILoadoutGroupFilesViewModel CreateViewModel(LoadoutGroupFilesPageContext context) - { - var vm = ServiceProvider.GetRequiredService(); - vm.Context = context; - return vm; - } -} diff --git a/src/NexusMods.App.UI/Pages/LoadoutPage/ILoadoutViewModel.cs b/src/NexusMods.App.UI/Pages/LoadoutPage/ILoadoutViewModel.cs index c31062cb94..69da5d2fd5 100644 --- a/src/NexusMods.App.UI/Pages/LoadoutPage/ILoadoutViewModel.cs +++ b/src/NexusMods.App.UI/Pages/LoadoutPage/ILoadoutViewModel.cs @@ -1,3 +1,4 @@ +using NexusMods.App.UI.Controls.Navigation; using NexusMods.App.UI.WorkspaceSystem; namespace NexusMods.App.UI.Pages.LoadoutPage; @@ -8,7 +9,7 @@ public interface ILoadoutViewModel : IPageViewModelInterface R3.ReactiveCommand SwitchViewCommand { get; } - R3.ReactiveCommand ViewFilesCommand { get; } + R3.ReactiveCommand ViewFilesCommand { get; } R3.ReactiveCommand RemoveItemCommand { get; } } diff --git a/src/NexusMods.App.UI/Pages/LoadoutPage/LoadoutViewModel.cs b/src/NexusMods.App.UI/Pages/LoadoutPage/LoadoutViewModel.cs index e0f24222fb..77a0871bc4 100644 --- a/src/NexusMods.App.UI/Pages/LoadoutPage/LoadoutViewModel.cs +++ b/src/NexusMods.App.UI/Pages/LoadoutPage/LoadoutViewModel.cs @@ -1,15 +1,23 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reactive.Linq; using Avalonia.Controls.Models.TreeDataGrid; using DynamicData; +using DynamicData.Binding; +using DynamicData.Kernel; using Microsoft.Extensions.DependencyInjection; using NexusMods.Abstractions.Loadouts; using NexusMods.App.UI.Controls; +using NexusMods.App.UI.Controls.Navigation; +using NexusMods.App.UI.Controls.Trees; using NexusMods.App.UI.Extensions; +using NexusMods.App.UI.Pages.ItemContentsFileTree; using NexusMods.App.UI.Windows; using NexusMods.App.UI.WorkspaceSystem; using NexusMods.Icons; using NexusMods.MnemonicDB.Abstractions; using NexusMods.MnemonicDB.Abstractions.ElementComparers; +using NexusMods.MnemonicDB.Abstractions.IndexSegments; using NexusMods.MnemonicDB.Abstractions.TxFunctions; using ObservableCollections; using R3; @@ -23,7 +31,7 @@ public class LoadoutViewModel : APageViewModel, ILoadoutViewM public ReactiveCommand SwitchViewCommand { get; } - public ReactiveCommand ViewFilesCommand { get; } + public ReactiveCommand ViewFilesCommand { get; } public ReactiveCommand RemoveItemCommand { get; } public LoadoutTreeDataGridAdapter Adapter { get; } @@ -37,66 +45,108 @@ public LoadoutViewModel(IWindowManager windowManager, IServiceProvider servicePr _connection = serviceProvider.GetRequiredService(); - SwitchViewCommand = new ReactiveCommand(_ => - { - Adapter.ViewHierarchical.Value = !Adapter.ViewHierarchical.Value; - }); + SwitchViewCommand = new ReactiveCommand(_ => { Adapter.ViewHierarchical.Value = !Adapter.ViewHierarchical.Value; }); var hasSelection = Adapter.SelectedModels .ObserveCountChanged() .Select(count => count > 0); - ViewFilesCommand = hasSelection.ToReactiveCommand(_ => - { - // TODO: - }); + var viewModFilesArgumentsSubject = new BehaviorSubject>(Optional.None); - RemoveItemCommand = hasSelection.ToReactiveCommand(async (_, cancellationToken) => - { - var ids = Adapter.SelectedModels - .SelectMany(itemModel => itemModel.GetLoadoutItemIds()) - .ToHashSet(); + ViewFilesCommand = viewModFilesArgumentsSubject + .Select(viewModFilesArguments => viewModFilesArguments.HasValue) + .ToReactiveCommand( info => + { + var group = viewModFilesArgumentsSubject.Value; + if (!group.HasValue) return; - using var tx = _connection.BeginTransaction(); + var pageData = new PageData + { + FactoryId = ItemContentsFileTreePageFactory.StaticId, + Context = new ItemContentsFileTreePageContext + { + GroupId = group.Value.Id, + }, + }; + var workspaceController = GetWorkspaceController(); + var behavior = workspaceController.GetOpenPageBehavior(pageData, info); + workspaceController.OpenPage(workspaceController.ActiveWorkspaceId, pageData, behavior); + }, + false + ); - foreach (var id in ids) + RemoveItemCommand = hasSelection.ToReactiveCommand(async (_, cancellationToken) => { - tx.Delete(id, recursive: true); - } - - await tx.Commit(); - }, awaitOperation: AwaitOperation.Sequential, initialCanExecute: false, configureAwait: false); + var ids = Adapter.SelectedModels + .SelectMany(itemModel => itemModel.GetLoadoutItemIds()) + .ToHashSet(); - this.WhenActivated(disposables => - { - Adapter.Activate(); - Disposable.Create(Adapter, static adapter => adapter.Deactivate()).AddTo(disposables); + using var tx = _connection.BeginTransaction(); - // TODO: can be optimized with chunking or debounce - Adapter.MessageSubject - .SubscribeAwait(async (message, cancellationToken) => + foreach (var id in ids) { - using var tx = _connection.BeginTransaction(); + tx.Delete(id, recursive: true); + } - foreach (var id in message.Ids) - { - tx.Add(id, static (tx, db, loadoutItemId) => + await tx.Commit(); + }, + awaitOperation: AwaitOperation.Sequential, + initialCanExecute: false, + configureAwait: false + ); + + this.WhenActivated(disposables => + { + Adapter.Activate(); + Disposable.Create(Adapter, static adapter => adapter.Deactivate()).AddTo(disposables); + + // TODO: can be optimized with chunking or debounce + Adapter.MessageSubject + .SubscribeAwait(async (message, cancellationToken) => { - var loadoutItem = LoadoutItem.Load(db, loadoutItemId); - if (loadoutItem.IsDisabled) - { - tx.Retract(loadoutItemId, LoadoutItem.Disabled, Null.Instance); - } else + using var tx = _connection.BeginTransaction(); + + foreach (var id in message.Ids) { - tx.Add(loadoutItemId, LoadoutItem.Disabled, Null.Instance); + tx.Add(id, + static (tx, db, loadoutItemId) => + { + var loadoutItem = LoadoutItem.Load(db, loadoutItemId); + if (loadoutItem.IsDisabled) + { + tx.Retract(loadoutItemId, LoadoutItem.Disabled, Null.Instance); + } + else + { + tx.Add(loadoutItemId, LoadoutItem.Disabled, Null.Instance); + } + } + ); } - }); - } - await tx.Commit(); - }, awaitOperation: AwaitOperation.Parallel, configureAwait: false) - .AddTo(disposables); - }); + await tx.Commit(); + }, + awaitOperation: AwaitOperation.Parallel, + configureAwait: false + ) + .AddTo(disposables); + + // Compute the target group for the ViewFilesCommand + Adapter.SelectedModels.ObserveCountChanged() + .Select(this, static (count, vm) => count == 1 ? vm.Adapter.SelectedModels[0] : null) + .ObserveOnThreadPool() + .Select(_connection, + static (model, connection) => + { + if (model is null) return Optional.None; + return LoadoutItemGroupFileTreeViewModel.GetViewModFilesLoadoutItemGroup(model.GetLoadoutItemIds(), connection); + } + ) + .ObserveOnUIThreadDispatcher() + .Subscribe(viewModFilesArgumentsSubject.OnNext) + .AddTo(disposables); + } + ); } } @@ -112,32 +162,34 @@ public class LoadoutTreeDataGridAdapter : TreeDataGridAdapter _commandDisposables = new(); private readonly IDisposable _activationDisposable; + public LoadoutTreeDataGridAdapter(IServiceProvider serviceProvider) { _loadoutDataProviders = serviceProvider.GetServices().ToArray(); _connection = serviceProvider.GetRequiredService(); _activationDisposable = this.WhenActivated(static (adapter, disposables) => - { - Disposable.Create(adapter._commandDisposables, static commandDisposables => { - foreach (var kv in commandDisposables) - { - var (_, disposable) = kv; - disposable.Dispose(); - } + Disposable.Create(adapter._commandDisposables, + static commandDisposables => + { + foreach (var kv in commandDisposables) + { + var (_, disposable) = kv; + disposable.Dispose(); + } - commandDisposables.Clear(); - }).AddTo(disposables); - }); + commandDisposables.Clear(); + } + ) + .AddTo(disposables); + } + ); } protected override void BeforeModelActivationHook(LoadoutItemModel model) { - var disposable = model.ToggleEnableStateCommand.Subscribe(MessageSubject, static (ids, subject) => - { - subject.OnNext(new ToggleEnableState(ids)); - }); + var disposable = model.ToggleEnableStateCommand.Subscribe(MessageSubject, static (ids, subject) => { subject.OnNext(new ToggleEnableState(ids)); }); var didAdd = _commandDisposables.TryAdd(model, disposable); Debug.Assert(didAdd, "subscription for the model shouldn't exist yet"); @@ -185,6 +237,7 @@ protected override IColumn[] CreateColumns(bool viewHierarchic } private bool _isDisposed; + protected override void Dispose(bool disposing) { if (!_isDisposed) diff --git a/src/NexusMods.App.UI/Services.cs b/src/NexusMods.App.UI/Services.cs index e2756b5d64..615ddda793 100644 --- a/src/NexusMods.App.UI/Services.cs +++ b/src/NexusMods.App.UI/Services.cs @@ -46,12 +46,12 @@ using NexusMods.App.UI.Pages.Diagnostics; using NexusMods.App.UI.Pages.Diff.ApplyDiff; using NexusMods.App.UI.Pages.Downloads; +using NexusMods.App.UI.Pages.ItemContentsFileTree; using NexusMods.App.UI.Pages.LibraryPage; using NexusMods.App.UI.Pages.LoadoutGrid; using NexusMods.App.UI.Pages.LoadoutGrid.Columns.ModEnabled; using NexusMods.App.UI.Pages.LoadoutGrid.Columns.ModInstalled; using NexusMods.App.UI.Pages.LoadoutGrid.Columns.ModName; -using NexusMods.App.UI.Pages.LoadoutGroupFiles; using NexusMods.App.UI.Pages.LoadoutPage; using NexusMods.App.UI.Pages.ModLibrary; using NexusMods.App.UI.Pages.MyGames; @@ -217,8 +217,8 @@ public static IServiceCollection AddUI(this IServiceCollection c) .AddView() .AddViewModel() - .AddView() - .AddViewModel() + .AddView() + .AddViewModel() .AddView() .AddView() @@ -254,7 +254,7 @@ public static IServiceCollection AddUI(this IServiceCollection c) .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/NexusMods.App.UI/TypeFinder.cs b/src/NexusMods.App.UI/TypeFinder.cs index da8553a52c..f3540c9f56 100644 --- a/src/NexusMods.App.UI/TypeFinder.cs +++ b/src/NexusMods.App.UI/TypeFinder.cs @@ -3,9 +3,9 @@ using NexusMods.App.UI.Pages.Diagnostics; using NexusMods.App.UI.Pages.Diff.ApplyDiff; using NexusMods.App.UI.Pages.Downloads; +using NexusMods.App.UI.Pages.ItemContentsFileTree; using NexusMods.App.UI.Pages.LibraryPage; using NexusMods.App.UI.Pages.LoadoutGrid; -using NexusMods.App.UI.Pages.LoadoutGroupFiles; using NexusMods.App.UI.Pages.LoadoutPage; using NexusMods.App.UI.Pages.ModLibrary; using NexusMods.App.UI.Pages.MyGames; @@ -36,7 +36,7 @@ public IEnumerable DescendentsOf(Type type) typeof(FileOriginsPageContext), typeof(TextEditorPageContext), typeof(MyLoadoutsPageContext), - typeof(LoadoutGroupFilesPageContext), + typeof(ItemContentsFileTreePageContext), typeof(LibraryPageContext), typeof(LoadoutPageContext),