Skip to content

Commit

Permalink
Merge pull request #1985 from erri120/feat/1864-8
Browse files Browse the repository at this point in the history
Remaining functionality for new library and loadout pages
  • Loading branch information
erri120 authored Sep 10, 2024
2 parents 67d6e3c + cc05a9a commit 6a00a4b
Show file tree
Hide file tree
Showing 16 changed files with 284 additions and 117 deletions.
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<PackageVersion Include="NLog" Version="5.2.8" />
<PackageVersion Include="Noggog.CSharpExt" Version="2.64.0" />
<PackageVersion Include="ObservableCollections" Version="2.2.0" />
<PackageVersion Include="ObservableCollections.R3" Version="2.2.0" />
<PackageVersion Include="ObservableCollections.R3" Version="3.0.1" />
<PackageVersion Include="Polly" Version="8.4.1" />
<PackageVersion Include="R3" Version="1.2.8" />
<PackageVersion Include="R3Extensions.Avalonia" Version="1.2.8" />
Expand Down Expand Up @@ -130,4 +130,4 @@
<PackageVersion Include="Splat.Microsoft.Extensions.Logging" Version="15.0.1" />
<PackageVersion Include="TransparentValueObjects" Version="1.0.1" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public partial class Loadout : IModelDefinition
/// All items in the Loadout.
/// </summary>
public static readonly BackReferenceAttribute<LoadoutItem> Items = new(LoadoutItem.Loadout);

public partial struct ReadOnly
{
/// <summary>
Expand Down
31 changes: 17 additions & 14 deletions src/NexusMods.App.UI/Controls/TreeDataGrid/TreeDataGridAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ public abstract class TreeDataGridAdapter<TModel, TKey> : ReactiveR3Object
public BindableReactiveProperty<bool> ViewHierarchical { get; } = new(value: true);
public BindableReactiveProperty<bool> IsSourceEmpty { get; } = new(value: true);

public ObservableList<TModel> SelectedModels { get; private set; } = [];
public ObservableHashSet<TModel> SelectedModels { get; private set; } = [];

private ObservableList<TModel> Roots { get; set; } = [];
private ISynchronizedView<TModel, TModel> RootsView { get; }
private INotifyCollectionChangedSynchronizedView<TModel> RootsCollectionChangedView { get; }
private INotifyCollectionChangedSynchronizedViewList<TModel> RootsCollectionChangedView { get; }

private readonly IDisposable _activationDisposable;
private readonly SerialDisposable _selectionModelsSerialDisposable = new();
Expand Down Expand Up @@ -60,13 +60,25 @@ protected TreeDataGridAdapter()

self.ViewHierarchical
.AsObservable()
.Select(self, static (viewHierarchical, self) =>
.Do(self, static (viewHierarchical, self) =>
{
self.Roots.Clear();

// NOTE(erri120): we have to do this manually, the TreeDataGrid doesn't deselect when changing source
self.SelectedModels.Clear();

var (source, selectionObservable) = self.CreateSource(self.RootsCollectionChangedView, createHierarchicalSource: viewHierarchical);

self._selectionModelsSerialDisposable.Disposable = selectionObservable.Subscribe(self, static (eventArgs, self) =>
{
self.SelectedModels.RemoveRange(eventArgs.DeselectedItems.NotNull());
self.SelectedModels.AddRange(eventArgs.SelectedItems.NotNull());
});

self.Source.Value = source;
})
.Select(self, static (viewHierarchical, self) =>
{
return self
.GetRootsObservable(viewHierarchical)
.OnUI()
Expand All @@ -76,17 +88,8 @@ protected TreeDataGridAdapter()
.Select(viewHierarchical, static (_, viewHierarchical) => viewHierarchical);
})
.Switch()
.Select(self, static (viewHierarchical, self) => self.CreateSource(self.RootsCollectionChangedView, createHierarchicalSource: viewHierarchical))
.Subscribe(self, static (tuple, self) =>
{
self._selectionModelsSerialDisposable.Disposable = tuple.selectionObservable.Subscribe(self, static (eventArgs, self) =>
{
self.SelectedModels.Remove(eventArgs.DeselectedItems.NotNull());
self.SelectedModels.AddRange(eventArgs.SelectedItems.NotNull());
});

self.Source.Value = tuple.source;
}).AddTo(disposables);
.Subscribe()
.AddTo(disposables);

Disposable.Create(self._selectionModelsSerialDisposable, static serialDisposable => serialDisposable.Disposable = null).AddTo(disposables);
});
Expand Down
23 changes: 13 additions & 10 deletions src/NexusMods.App.UI/Controls/TreeDataGrid/TreeDataGridItemModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ public class TreeDataGridItemModel<TModel, TKey> : TreeDataGridItemModel
public BindableReactiveProperty<bool> HasChildren { get; } = new();

public IObservable<IChangeSet<TModel, TKey>> ChildrenObservable { get; init; } = Observable.Empty<IChangeSet<TModel, TKey>>();

private ObservableList<TModel> _children = [];
private readonly INotifyCollectionChangedSynchronizedView<TModel> _childrenView;
private readonly INotifyCollectionChangedSynchronizedViewList<TModel> _childrenView;

private readonly BehaviorSubject<bool> _childrenCollectionInitialization = new(initialValue: false);

Expand Down Expand Up @@ -59,7 +60,7 @@ public bool IsExpanded

protected TreeDataGridItemModel()
{
_childrenView = _children.CreateView(static model => model).ToNotifyCollectionChanged();
_childrenView = _children.ToNotifyCollectionChanged();

_modelActivationDisposable = WhenModelActivated(this, static (model, disposables) =>
{
Expand Down Expand Up @@ -91,14 +92,11 @@ protected TreeDataGridItemModel()
.DistinctUntilChanged()
.Subscribe(model, onNext: static (isInitializing, model) =>
{
// NOTE(erri120): We always need to reset when the observable triggers.
// Note that the observable we're currently in with the `DistinctUntilChanged`
// gets disposed when the model is deactivated. This is important to
// understand for the model and child activation/deactivation relationships.
model._childrenObservableSerialDisposable.Disposable = null;
CleanupChildren(model._children);

if (isInitializing)
// NOTE(erri120): Lazy-init the subscription. Previously, we'd re-subscribe to the children observable
// and clear all previous values. This broke the TreeDataGrid selection code. Instead, we'll have a lazy
// observable. When the parent gets expanded for the first time, we'll set up this subscription and keep
// it alive for the entire lifetime of the parent.
if (isInitializing && model._childrenObservableSerialDisposable.Disposable is null)
{
model._childrenObservableSerialDisposable.Disposable = model.ChildrenObservable
.OnUI()
Expand All @@ -112,6 +110,11 @@ protected TreeDataGridItemModel()

private static void CleanupChildren(ObservableList<TModel> children)
{
foreach (var child in children)
{
child.Dispose();
}

children.Clear();
}

Expand Down
52 changes: 50 additions & 2 deletions src/NexusMods.App.UI/Pages/ILibraryDataProvider.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,60 @@
using System.Reactive.Linq;
using DynamicData;
using NexusMods.Abstractions.GameLocators;
using NexusMods.Abstractions.Library.Models;
using NexusMods.Abstractions.Loadouts;
using NexusMods.App.UI.Pages.LibraryPage;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.MnemonicDB.Abstractions.DatomIterators;
using NexusMods.MnemonicDB.Abstractions.Query;

namespace NexusMods.App.UI.Pages;

public interface ILibraryDataProvider
{
IObservable<IChangeSet<LibraryItemModel, EntityId>> ObserveFlatLibraryItems();
IObservable<IChangeSet<LibraryItemModel, EntityId>> ObserveFlatLibraryItems(LibraryFilter libraryFilter);

IObservable<IChangeSet<LibraryItemModel, EntityId>> ObserveNestedLibraryItems();
IObservable<IChangeSet<LibraryItemModel, EntityId>> ObserveNestedLibraryItems(LibraryFilter libraryFilter);
}

public class LibraryFilter
{
public IObservable<LoadoutId> LoadoutObservable { get; }

public IObservable<ILocatableGame> GameObservable { get; }

public LibraryFilter(IObservable<LoadoutId> loadoutObservable, IObservable<ILocatableGame> gameObservable)
{
LoadoutObservable = loadoutObservable;
GameObservable = gameObservable;
}
}

public static class QueryHelper
{
/// <summary>
/// Filters the source of <see cref="LibraryLinkedLoadoutItem"/> to only contain items
/// that are installed in the loadout using <see cref="LibraryFilter.LoadoutObservable"/>.
/// </summary>
public static IObservable<IChangeSet<Datom, EntityId>> FilterInObservableLoadout(
this IObservable<IChangeSet<Datom, EntityId>> source,
IConnection connection,
LibraryFilter libraryFilter)
{
return source.FilterOnObservable((_, e) => libraryFilter.LoadoutObservable.Select(loadoutId =>
LoadoutItem.Load(connection.Db, e).LoadoutId.Equals(loadoutId))
);
}

public static IObservable<IChangeSet<LibraryLinkedLoadoutItem.ReadOnly, EntityId>> GetLinkedLoadoutItems(
IConnection connection,
LibraryItemId libraryItemId,
LibraryFilter libraryFilter)
{
return connection
.ObserveDatoms(LibraryLinkedLoadoutItem.LibraryItemId, libraryItemId)
.AsEntityIds()
.FilterInObservableLoadout(connection, libraryFilter)
.Transform((_, entityId) => LibraryLinkedLoadoutItem.Load(connection.Db, entityId));
}
}
16 changes: 15 additions & 1 deletion src/NexusMods.App.UI/Pages/ILoadoutDataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,30 @@
using NexusMods.Abstractions.MnemonicDB.Attributes.Extensions;
using NexusMods.App.UI.Pages.LoadoutPage;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.MnemonicDB.Abstractions.DatomIterators;

namespace NexusMods.App.UI.Pages;

public interface ILoadoutDataProvider
{
IObservable<IChangeSet<LoadoutItemModel, EntityId>> ObserveNestedLoadoutItems();
IObservable<IChangeSet<LoadoutItemModel, EntityId>> ObserveNestedLoadoutItems(LoadoutFilter loadoutFilter);
}

public class LoadoutFilter
{
public required LoadoutId LoadoutId { get; init; }
}

public static class LoadoutDataProviderHelper
{
public static IObservable<IChangeSet<Datom, EntityId>> FilterInStaticLoadout(
this IObservable<IChangeSet<Datom, EntityId>> source,
IConnection connection,
LoadoutFilter loadoutFilter)
{
return source.Filter(datom => LoadoutItem.Load(connection.Db, datom.E).LoadoutId.Equals(loadoutFilter.LoadoutId));
}

public static LoadoutItemModel ToLoadoutItemModel(IConnection connection, LibraryLinkedLoadoutItem.ReadOnly libraryLinkedLoadoutItem)
{
// NOTE(erri120): We'll only show the library linked loadout item group for now.
Expand Down
14 changes: 10 additions & 4 deletions src/NexusMods.App.UI/Pages/Library/LibraryItemRemover.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using NexusMods.App.UI.Overlays;
using NexusMods.App.UI.Overlays.LibraryDeleteConfirmation;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.MnemonicDB.Abstractions.IndexSegments;
using NexusMods.MnemonicDB.Abstractions.TxFunctions;
namespace NexusMods.App.UI.Pages.Library;

Expand All @@ -17,13 +18,18 @@ namespace NexusMods.App.UI.Pages.Library;
/// </remarks>
public static class LibraryItemRemover
{
public static async Task RemoveAsync(IConnection conn, IOverlayController overlayController, ILibraryService libraryService, LibraryItem.ReadOnly[] toRemove)
public static async Task RemoveAsync(
IConnection conn,
IOverlayController overlayController,
ILibraryService libraryService,
LibraryItem.ReadOnly[] toRemove,
CancellationToken cancellationToken = default)
{
var warnings = LibraryItemDeleteWarningDetector.Process(conn, toRemove);
var alphaWarningViewModel = LibraryItemDeleteConfirmationViewModel.FromWarningDetector(warnings);
var controller = overlayController;
alphaWarningViewModel.Controller = controller;
var result = await controller.EnqueueAndWait(alphaWarningViewModel);
alphaWarningViewModel.Controller = overlayController;
var result = await overlayController.EnqueueAndWait(alphaWarningViewModel);
if (!result) return;

if (result)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public interface ILibraryViewModel : IPageViewModelInterface

ReactiveCommand<Unit> InstallSelectedItemsCommand { get; }
ReactiveCommand<Unit> InstallSelectedItemsWithAdvancedInstallerCommand { get; }
ReactiveCommand<Unit> RemoveSelectedItemsCommand { get; }

ReactiveCommand<Unit> OpenFilePickerCommand { get; }
ReactiveCommand<Unit> OpenNexusModsCommand { get; }
Expand Down
13 changes: 7 additions & 6 deletions src/NexusMods.App.UI/Pages/LibraryPage/LibraryItemModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ namespace NexusMods.App.UI.Pages.LibraryPage;
public class LibraryItemModel : TreeDataGridItemModel<LibraryItemModel, EntityId>
{
public required string Name { get; init; }
public required DateTime CreatedAt { get; init; }

// TODO: turn this back into a `Size`
// NOTE(erri120): requires https://github.com/AvaloniaUI/Avalonia.Controls.TreeDataGrid/pull/304
Expand All @@ -29,6 +28,7 @@ public class LibraryItemModel : TreeDataGridItemModel<LibraryItemModel, EntityId
private ObservableList<LibraryLinkedLoadoutItem.ReadOnly> LinkedLoadoutItems { get; set; } = [];

public ReactiveProperty<DateTime> InstalledDate { get; } = new(DateTime.UnixEpoch);
public ReactiveProperty<DateTime> CreatedAtDate { get; } = new(DateTime.UnixEpoch);

public Observable<DateTime>? Ticker { get; set; }
public BindableReactiveProperty<string> FormattedCreatedAtDate { get; } = new("-");
Expand All @@ -52,13 +52,13 @@ public LibraryItemModel(LibraryItemId libraryItemId)

_modelActivationDisposable = WhenModelActivated(this, static (model, disposables) =>
{
model.FormattedCreatedAtDate.Value = FormatDate(DateTime.Now, model.CreatedAt);
model.FormattedCreatedAtDate.Value = FormatDate(DateTime.Now, model.CreatedAtDate.Value);
model.FormattedInstalledDate.Value = FormatDate(DateTime.Now, model.InstalledDate.Value);

Debug.Assert(model.Ticker is not null, "should've been set before activation");
model.Ticker.Subscribe(model, static (now, model) =>
{
model.FormattedCreatedAtDate.Value = FormatDate(now, model.CreatedAt);
model.FormattedCreatedAtDate.Value = FormatDate(now, model.CreatedAtDate.Value);
model.FormattedInstalledDate.Value = FormatDate(now, model.InstalledDate.Value);
}).AddTo(disposables);

Expand All @@ -78,6 +78,7 @@ public LibraryItemModel(LibraryItemId libraryItemId)
model.InstallText.Value = "Install";
model.IsInstalledInLoadout.Value = false;
model.InstalledDate.Value = DateTime.UnixEpoch;
model.FormattedInstalledDate.Value = "-";
}
}).AddTo(disposables);

Expand All @@ -86,7 +87,7 @@ public LibraryItemModel(LibraryItemId libraryItemId)
});
}

private static string FormatDate(DateTime now, DateTime date)
protected static string FormatDate(DateTime now, DateTime date)
{
if (date == DateTime.UnixEpoch || date == default(DateTime)) return "-";
return date.Humanize(dateToCompareAgainst: now);
Expand Down Expand Up @@ -185,8 +186,8 @@ public static IColumn<LibraryItemModel> CreateAddedAtColumn()
getter: model => model.FormattedCreatedAtDate.Value,
options: new TextColumnOptions<LibraryItemModel>
{
CompareAscending = static (a, b) => a?.CreatedAt.CompareTo(b?.CreatedAt ?? DateTime.UnixEpoch) ?? 1,
CompareDescending = static (a, b) => b?.CreatedAt.CompareTo(a?.CreatedAt ?? DateTime.UnixEpoch) ?? 1,
CompareAscending = static (a, b) => a?.CreatedAtDate.Value.CompareTo(b?.CreatedAtDate.Value ?? DateTime.UnixEpoch) ?? 1,
CompareDescending = static (a, b) => b?.CreatedAtDate.Value.CompareTo(a?.CreatedAtDate.Value ?? DateTime.UnixEpoch) ?? 1,
IsTextSearchEnabled = false,
CanUserResizeColumn = true,
CanUserSortColumn = true,
Expand Down
11 changes: 10 additions & 1 deletion src/NexusMods.App.UI/Pages/LibraryPage/LibraryView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,22 @@
<!-- Toolbar -->
<Border Grid.Row="0" Classes="Toolbar">
<StackPanel Orientation="Horizontal">
<!-- Add Buttons Section -->
<Button x:Name="SwitchView">
<StackPanel>
<TextBlock>Switch View</TextBlock>
</StackPanel>
</Button>

<Button x:Name="RemoveModButton">
<ToolTip.Tip>
<TextBlock Text="{x:Static resources:Language.FileOriginPage_RemoveMod_ToolTip}" />
</ToolTip.Tip>
<StackPanel>
<icons:UnifiedIcon Classes="DeleteOutline" />
<TextBlock Text="{x:Static resources:Language.FileOriginsPage__Delete_Mod}"/>
</StackPanel>
</Button>

<Button x:Name="AddModButton">
<ToolTip.Tip>
<TextBlock Text="{x:Static resources:Language.FileOriginPage_AddMod_ToolTip}" />
Expand Down
3 changes: 3 additions & 0 deletions src/NexusMods.App.UI/Pages/LibraryPage/LibraryView.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public LibraryView()
this.BindCommand(ViewModel, vm => vm.SwitchViewCommand, view => view.SwitchView)
.AddTo(disposables);

this.BindCommand(ViewModel, vm => vm.RemoveSelectedItemsCommand, view => view.RemoveModButton)
.AddTo(disposables);

this.BindCommand(ViewModel, vm => vm.InstallSelectedItemsCommand, view => view.AddModButton)
.AddTo(disposables);

Expand Down
Loading

0 comments on commit 6a00a4b

Please sign in to comment.