Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Library: Use new columns #2151

Merged
merged 3 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace NexusMods.App.UI.Controls;
/// Adapter class for working with <see cref="TreeDataGrid"/>.
/// </summary>
public abstract class TreeDataGridAdapter<TModel, TKey> : ReactiveR3Object
where TModel : TreeDataGridItemModel<TModel, TKey>
where TModel : class, ITreeDataGridItemModel<TModel, TKey>
where TKey : notnull
{
public Subject<(TModel model, bool isActivating)> ModelActivationSubject { get; } = new();
Expand Down
53 changes: 34 additions & 19 deletions src/NexusMods.App.UI/Controls/TreeDataGrid/TreeDataGridItemModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,51 @@
namespace NexusMods.App.UI.Controls;

[PublicAPI]
public interface ITreeDataGridItemModel : IReactiveR3Object;
public interface ITreeDataGridItemModel : IReactiveR3Object
{
ReactiveProperty<bool> IsSelected { get; }
}

/// <summary>
/// Base class for models of <see cref="Avalonia.Controls.TreeDataGrid"/> items.
/// </summary>
public class TreeDataGridItemModel : ReactiveR3Object, ITreeDataGridItemModel;
public class TreeDataGridItemModel : ReactiveR3Object, ITreeDataGridItemModel
{
public ReactiveProperty<bool> IsSelected { get; } = new(value: false);
}

public interface ITreeDataGridItemModel<out TModel, TKey> : ITreeDataGridItemModel
where TModel : class, ITreeDataGridItemModel<TModel, TKey>
where TKey : notnull
{
BindableReactiveProperty<bool> HasChildren { get; }

IEnumerable<TModel> Children { get; }

bool IsExpanded { get; [UsedImplicitly] set; }

public static HierarchicalExpanderColumn<TModel> CreateExpanderColumn(IColumn<TModel> innerColumn)
{
return new HierarchicalExpanderColumn<TModel>(
inner: innerColumn,
childSelector: static model => model.Children,
hasChildrenSelector: static model => model.HasChildren.Value,
isExpandedSelector: static model => model.IsExpanded
)
{
Tag = "expander",
};
}
}

/// <summary>
/// Generic variant of <see cref="TreeDataGridItemModel"/>.
/// </summary>
[PublicAPI]
public class TreeDataGridItemModel<TModel, TKey> : TreeDataGridItemModel
where TModel : TreeDataGridItemModel<TModel, TKey>
public class TreeDataGridItemModel<TModel, TKey> : TreeDataGridItemModel, ITreeDataGridItemModel<TModel, TKey>
where TModel : class, ITreeDataGridItemModel<TModel, TKey>
where TKey : notnull
{
public ReactiveProperty<bool> IsSelected { get; } = new(value: false);

public IObservable<bool> HasChildrenObservable { get; init; } = Observable.Return(false);
public BindableReactiveProperty<bool> HasChildren { get; } = new();

Expand Down Expand Up @@ -152,17 +180,4 @@ [MustDisposeResource] protected static IDisposable WhenModelActivated<TItemModel
{
return model.WhenActivated(block);
}

public static HierarchicalExpanderColumn<TModel> CreateExpanderColumn(IColumn<TModel> innerColumn)
{
return new HierarchicalExpanderColumn<TModel>(
inner: innerColumn,
childSelector: static model => model.Children,
hasChildrenSelector: static model => model.HasChildren.Value,
isExpandedSelector: static model => model.IsExpanded
)
{
Tag = "expander",
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static void SetupTreeDataGridAdapter<TView, TViewModel, TItemModel, TKey>
Func<TViewModel, TreeDataGridAdapter<TItemModel, TKey>> getAdapter)
where TView : ReactiveUserControl<TViewModel>
where TViewModel : class, IViewModelInterface
where TItemModel : TreeDataGridItemModel<TItemModel, TKey>
where TItemModel : class, ITreeDataGridItemModel<TItemModel, TKey>
where TKey : notnull
{
treeDataGrid.ElementFactory = new CustomElementFactory();
Expand Down
30 changes: 30 additions & 0 deletions src/NexusMods.App.UI/Extensions/FormatExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Globalization;
using Humanizer;
using Humanizer.Bytes;
using NexusMods.Paths;
using R3;

namespace NexusMods.App.UI.Extensions;

public static class FormatExtensions
{
public static string FormatDate(this DateTimeOffset date, DateTimeOffset now)
{
if (date == DateTimeOffset.MinValue || date == DateTimeOffset.MaxValue || date == DateTimeOffset.UnixEpoch) return "-";
return date.Humanize(dateToCompareAgainst: now, culture: CultureInfo.CurrentUICulture);
}

public static BindableReactiveProperty<string> ToFormattedProperty(this Observable<DateTimeOffset> source)
{
return source
.Select(static date => date.FormatDate(now: TimeProvider.System.GetLocalNow()))
.ToBindableReactiveProperty(initialValue: "");
}

public static BindableReactiveProperty<string> ToFormattedProperty(this Observable<Size> source)
{
return source
.Select(static size => ByteSize.FromBytes(size.Value).Humanize())
.ToBindableReactiveProperty(initialValue: "");
}
}
23 changes: 20 additions & 3 deletions src/NexusMods.App.UI/Extensions/R3Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,40 @@ [MustDisposeResource] public static IDisposable WhenActivated<T>(
this T obj,
Action<T, CompositeDisposable> block)
where T : IReactiveR3Object
{
return WhenActivated(obj, state: block, static (obj, block, disposables) =>
{
block(obj, disposables);
});
}

/// <summary>
/// Provides an activation block for <see cref="ReactiveR3Object"/>.
/// </summary>
[MustDisposeResource]
public static IDisposable WhenActivated<T, TState>(
this T obj,
TState state,
Action<T, TState, CompositeDisposable> block)
where T : IReactiveR3Object
where TState : notnull
{
var d = Disposable.CreateBuilder();

var serialDisposable = new SerialDisposable();
serialDisposable.AddTo(ref d);

obj.Activation.DistinctUntilChanged().Subscribe((obj, serialDisposable, block), onNext: static (isActivated, state) =>
obj.Activation.DistinctUntilChanged().Subscribe(((obj, state), serialDisposable, block), onNext: static (isActivated, state) =>
{
var (model, serialDisposable, block) = state;
var (wrapper, serialDisposable, block) = state;

serialDisposable.Disposable = null;
if (isActivated)
{
var compositeDisposable = new CompositeDisposable();
serialDisposable.Disposable = compositeDisposable;

block(model, compositeDisposable);
block(wrapper.obj, wrapper.state, compositeDisposable);
}
}, onCompleted: static (_, state) =>
{
Expand Down
6 changes: 0 additions & 6 deletions src/NexusMods.App.UI/NexusMods.App.UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -596,12 +596,6 @@
<Compile Update="Pages\LibraryPage\LibraryViewModel.cs">
<DependentUpon>ILibraryViewModel.cs</DependentUpon>
</Compile>
<Compile Update="Pages\LibraryPage\FakeParentLibraryItemModel.cs">
<DependentUpon>LibraryItemModel.cs</DependentUpon>
</Compile>
<Compile Update="Pages\LibraryPage\NexusModsModPageLibraryItemModel.cs">
<DependentUpon>LibraryItemModel.cs</DependentUpon>
</Compile>
<Compile Update="Pages\LoadoutPage\FakeParentLoadoutItemModel.cs">
<DependentUpon>LoadoutItemModel.cs</DependentUpon>
</Compile>
Expand Down
4 changes: 2 additions & 2 deletions src/NexusMods.App.UI/Pages/ILibraryDataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ namespace NexusMods.App.UI.Pages;

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

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

public class LibraryFilter
Expand Down

This file was deleted.

76 changes: 75 additions & 1 deletion src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,79 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using DynamicData;
using JetBrains.Annotations;
using NexusMods.Abstractions.Library.Models;
using NexusMods.Abstractions.Loadouts;
using NexusMods.Abstractions.MnemonicDB.Attributes.Extensions;
using NexusMods.App.UI.Controls;
using NexusMods.App.UI.Extensions;
using NexusMods.MnemonicDB.Abstractions;
using ObservableCollections;
using R3;

namespace NexusMods.App.UI.Pages.LibraryPage;

public interface ILibraryItemModel : ITreeDataGridItemModel;
public interface ILibraryItemModel : ITreeDataGridItemModel<ILibraryItemModel, EntityId>;

public interface IHasTicker
{
Observable<DateTimeOffset>? Ticker { get; set; }
}

public interface IHasLinkedLoadoutItems
{
IObservable<IChangeSet<LibraryLinkedLoadoutItem.ReadOnly, EntityId>> LinkedLoadoutItemsObservable { get; }
ObservableDictionary<EntityId, LibraryLinkedLoadoutItem.ReadOnly> LinkedLoadoutItems { get; }

[MustDisposeResource] static IDisposable SetupLinkedLoadoutItems<TModel>(TModel self, SerialDisposable serialDisposable)
where TModel : IHasLinkedLoadoutItems, ILibraryItemWithInstallAction, ILibraryItemWithInstalledDate
{
var disposable = self.LinkedLoadoutItems
.ObserveCountChanged(notifyCurrentCount: true)
.Subscribe(self, static (count, self) =>
{
var isInstalled = count > 0;
self.IsInstalled.Value = isInstalled;
self.InstallButtonText.Value = ILibraryItemWithInstallAction.GetButtonText(isInstalled);
self.InstalledDate.Value = isInstalled ? self.LinkedLoadoutItems.Select(static kv => kv.Value.GetCreatedAt()).Max() : DateTimeOffset.MinValue;
});

if (serialDisposable.Disposable is null)
{
serialDisposable.Disposable = self.LinkedLoadoutItemsObservable.OnUI().SubscribeWithErrorLogging(changes => self.LinkedLoadoutItems.ApplyChanges(changes));
}

return disposable;
}
}

public interface IIsParentLibraryItemModel : ILibraryItemModel
{
IReadOnlyList<LibraryItemId> LibraryItemIds { get; }
}

public interface IIsChildLibraryItemModel : ILibraryItemModel
{
LibraryItemId LibraryItemId { get; }
}

[SuppressMessage("ReSharper", "PossibleInterfaceMemberAmbiguity")]
public interface ILibraryItemWithDates : IHasTicker, ILibraryItemWithDownloadedDate, ILibraryItemWithInstalledDate
{
[MustDisposeResource]
static IDisposable SetupDates<TModel>(TModel self) where TModel : class, ILibraryItemWithDates
{
return self.WhenActivated(static (self, disposables) =>
{
Debug.Assert(self.Ticker is not null, "should've been set before activation");
self.Ticker.Subscribe(self, static (now, self) =>
{
ILibraryItemWithDownloadedDate.FormatDate(self, now: now);
ILibraryItemWithInstalledDate.FormatDate(self, now: now);
}).AddTo(disposables);

ILibraryItemWithDownloadedDate.FormatDate(self, now: TimeProvider.System.GetLocalNow());
ILibraryItemWithInstalledDate.FormatDate(self, now: TimeProvider.System.GetLocalNow());
});
}
}
Loading
Loading