diff --git a/src/NexusMods.App.UI/Controls/ReactiveR3Object.cs b/src/NexusMods.App.UI/Controls/ReactiveR3Object.cs index 5a918ab162..3297b0b592 100644 --- a/src/NexusMods.App.UI/Controls/ReactiveR3Object.cs +++ b/src/NexusMods.App.UI/Controls/ReactiveR3Object.cs @@ -5,18 +5,26 @@ namespace NexusMods.App.UI.Controls; +[PublicAPI] +public interface IReactiveR3Object : ReactiveUI.IReactiveObject, IDisposable +{ + Observable Activation { get; } + void Activate(); + void Deactivate(); +} + /// /// Base class using R3 with support for activation/deactivation, /// , and . /// [PublicAPI] -public class ReactiveR3Object : ReactiveUI.IReactiveObject, IDisposable +public class ReactiveR3Object : IReactiveR3Object { private readonly BehaviorSubject _activation = new(initialValue: false); public Observable Activation => _activation; - internal void Activate() => _activation.OnNext(true); - internal void Deactivate() + public void Activate() => _activation.OnNext(true); + public void Deactivate() { // NOTE(erri120): no need to deactivate disposed objects, as // any subscriptions and WhenActivated-blocks are already disposed diff --git a/src/NexusMods.App.UI/Controls/TreeDataGrid/IColumnDefinition.cs b/src/NexusMods.App.UI/Controls/TreeDataGrid/IColumnDefinition.cs new file mode 100644 index 0000000000..55a7358df1 --- /dev/null +++ b/src/NexusMods.App.UI/Controls/TreeDataGrid/IColumnDefinition.cs @@ -0,0 +1,70 @@ +using Avalonia.Controls.Models.TreeDataGrid; +using JetBrains.Annotations; + +namespace NexusMods.App.UI.Controls; + +/// +/// Static interface for column definitions +/// +[PublicAPI] +public interface IColumnDefinition + where TModel : class + where TSelf : IColumnDefinition, IComparable +{ + static virtual int Compare(TModel? a, TModel? b) + { + return (a, b) switch + { + (TSelf itemA, TSelf itemB) => itemA.CompareTo(itemB), + + // b precedes a + (TSelf, _) => 1, + + // a precedes b + (_, TSelf) => -1, + + // a and b are in the same position + (_, _) => 0, + }; + } + + static abstract string GetColumnHeader(); + static abstract string GetColumnTemplateResourceKey(); + static virtual string GetColumnId() => TSelf.GetColumnTemplateResourceKey(); + + static virtual IColumn CreateColumn() + { + return new CustomTemplateColumn( + header: TSelf.GetColumnHeader(), + cellTemplateResourceKey: TSelf.GetColumnTemplateResourceKey(), + options: new TemplateColumnOptions + { + CanUserSortColumn = true, + CanUserResizeColumn = true, + CompareAscending = static (a, b) => TSelf.Compare(a, b), + CompareDescending = static (a, b) => TSelf.Compare(b, a), + } + ) + { + Id = TSelf.GetColumnId(), + }; + } +} + +/// +/// Column helper. +/// +[PublicAPI] +public static class ColumnCreator +{ + /// + /// Creates a column from a static interface definition. + /// + public static IColumn CreateColumn() + where TModel : class + where TColumnInterface : IColumnDefinition, IComparable + { + return TColumnInterface.CreateColumn(); + } +} + diff --git a/src/NexusMods.App.UI/Controls/TreeDataGrid/TreeDataGridItemModel.cs b/src/NexusMods.App.UI/Controls/TreeDataGrid/TreeDataGridItemModel.cs index d7e39f26fb..acb8f18304 100644 --- a/src/NexusMods.App.UI/Controls/TreeDataGrid/TreeDataGridItemModel.cs +++ b/src/NexusMods.App.UI/Controls/TreeDataGrid/TreeDataGridItemModel.cs @@ -9,10 +9,13 @@ namespace NexusMods.App.UI.Controls; +[PublicAPI] +public interface ITreeDataGridItemModel : IReactiveR3Object; + /// /// Base class for models of items. /// -public class TreeDataGridItemModel : ReactiveR3Object; +public class TreeDataGridItemModel : ReactiveR3Object, ITreeDataGridItemModel; /// /// Generic variant of . @@ -145,7 +148,7 @@ protected override void Dispose(bool disposing) } [MustDisposeResource] protected static IDisposable WhenModelActivated(TItemModel model, Action block) - where TItemModel : TreeDataGridItemModel + where TItemModel : ITreeDataGridItemModel { return model.WhenActivated(block); } diff --git a/src/NexusMods.App.UI/Extensions/R3Extensions.cs b/src/NexusMods.App.UI/Extensions/R3Extensions.cs index b308b77b04..d2d2f07fdc 100644 --- a/src/NexusMods.App.UI/Extensions/R3Extensions.cs +++ b/src/NexusMods.App.UI/Extensions/R3Extensions.cs @@ -15,7 +15,7 @@ public static class R3Extensions [MustDisposeResource] public static IDisposable WhenActivated( this T obj, Action block) - where T : ReactiveR3Object + where T : IReactiveR3Object { var d = Disposable.CreateBuilder(); diff --git a/src/NexusMods.App.UI/NexusMods.App.UI.csproj b/src/NexusMods.App.UI/NexusMods.App.UI.csproj index 08f935af34..10b6f3cfd0 100644 --- a/src/NexusMods.App.UI/NexusMods.App.UI.csproj +++ b/src/NexusMods.App.UI/NexusMods.App.UI.csproj @@ -650,6 +650,24 @@ ICollectionCardViewModel.cs + + ILibraryItemModel.cs + + + ILibraryItemModel.cs + + + ILibraryItemModel.cs + + + ILibraryItemModel.cs + + + ILibraryItemModel.cs + + + ILibraryItemModel.cs + diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemModel.cs b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemModel.cs new file mode 100644 index 0000000000..e6f347190e --- /dev/null +++ b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemModel.cs @@ -0,0 +1,5 @@ +using NexusMods.App.UI.Controls; + +namespace NexusMods.App.UI.Pages.LibraryPage; + +public interface ILibraryItemModel : ITreeDataGridItemModel; diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithAction.cs b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithAction.cs new file mode 100644 index 0000000000..16cd32e8bb --- /dev/null +++ b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithAction.cs @@ -0,0 +1,35 @@ +using NexusMods.App.UI.Controls; +using R3; + +namespace NexusMods.App.UI.Pages.LibraryPage; + +public interface ILibraryItemWithAction : ILibraryItemModel, IComparable, IColumnDefinition +{ + int IComparable.CompareTo(ILibraryItemWithAction? other) + { + return (this, other) switch + { + (ILibraryItemWithInstallAction, ILibraryItemWithDownloadAction) => -1, + (ILibraryItemWithDownloadAction, ILibraryItemWithInstallAction) => 1, + _ => 0, + }; + } + + public const string ColumnTemplateResourceKey = "LibraryItemActionColumn"; + static string IColumnDefinition.GetColumnHeader() => "Action"; + static string IColumnDefinition.GetColumnTemplateResourceKey() => ColumnTemplateResourceKey; +} + +public interface ILibraryItemWithInstallAction : ILibraryItemWithAction +{ + ReactiveCommand InstallItemCommand { get; } + + BindableReactiveProperty IsInstalled { get; } + + BindableReactiveProperty InstallButtonText { get; } +} + +public interface ILibraryItemWithDownloadAction : ILibraryItemWithAction +{ + ReactiveCommand DownloadItemCommand { get; } +} diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithDownloadedDate.cs b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithDownloadedDate.cs new file mode 100644 index 0000000000..b1a7ca9b1c --- /dev/null +++ b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithDownloadedDate.cs @@ -0,0 +1,16 @@ +using NexusMods.App.UI.Controls; +using R3; + +namespace NexusMods.App.UI.Pages.LibraryPage; + +public interface ILibraryItemWithDownloadedDate : ILibraryItemModel, IComparable, IColumnDefinition +{ + ReactiveProperty DownloadedDate { get; } + BindableReactiveProperty FormattedDownloadedDate { get; } + + int IComparable.CompareTo(ILibraryItemWithDownloadedDate? other) => other is null ? 1 : DateTime.Compare(DownloadedDate.Value, other.DownloadedDate.Value); + + public const string ColumnTemplateResourceKey = "LibraryItemDownloadedDateColumn"; + static string IColumnDefinition.GetColumnHeader() => "Downloaded"; + static string IColumnDefinition.GetColumnTemplateResourceKey() => ColumnTemplateResourceKey; +} diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithInstalledDate.cs b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithInstalledDate.cs new file mode 100644 index 0000000000..9f4fa3d648 --- /dev/null +++ b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithInstalledDate.cs @@ -0,0 +1,16 @@ +using NexusMods.App.UI.Controls; +using R3; + +namespace NexusMods.App.UI.Pages.LibraryPage; + +public interface ILibraryItemWithInstalledDate : ILibraryItemModel, IComparable, IColumnDefinition +{ + ReactiveProperty InstalledDate { get; } + BindableReactiveProperty FormattedInstalledDate { get; } + + int IComparable.CompareTo(ILibraryItemWithInstalledDate? other) => other is null ? 1 : DateTime.Compare(InstalledDate.Value, other.InstalledDate.Value); + + public const string ColumnTemplateResourceKey = "LibraryItemInstalledDateColumn"; + static string IColumnDefinition.GetColumnHeader() => "Installed"; + static string IColumnDefinition.GetColumnTemplateResourceKey() => ColumnTemplateResourceKey; +} diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithName.cs b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithName.cs new file mode 100644 index 0000000000..73d5359131 --- /dev/null +++ b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithName.cs @@ -0,0 +1,15 @@ +using NexusMods.App.UI.Controls; +using R3; + +namespace NexusMods.App.UI.Pages.LibraryPage; + +public interface ILibraryItemWithName : ILibraryItemModel, IComparable, IColumnDefinition +{ + BindableReactiveProperty Name { get; } + + int IComparable.CompareTo(ILibraryItemWithName? other) => string.CompareOrdinal(Name.Value, other?.Name.Value); + + public const string ColumnTemplateResourceKey = "LibraryItemNameColumn"; + static string IColumnDefinition.GetColumnHeader() => "Name"; + static string IColumnDefinition.GetColumnTemplateResourceKey() => ColumnTemplateResourceKey; +} diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithSize.cs b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithSize.cs new file mode 100644 index 0000000000..8349be6166 --- /dev/null +++ b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithSize.cs @@ -0,0 +1,17 @@ +using NexusMods.App.UI.Controls; +using NexusMods.Paths; +using R3; + +namespace NexusMods.App.UI.Pages.LibraryPage; + +public interface ILibraryItemWithSize : ILibraryItemModel, IComparable, IColumnDefinition +{ + ReactiveProperty ItemSize { get; } + BindableReactiveProperty FormattedSize { get; } + + int IComparable.CompareTo(ILibraryItemWithSize? other) => other is null ? 1 : ItemSize.Value.CompareTo(other.ItemSize.Value); + + public const string ColumnTemplateResourceKey = "LibraryItemSizeColumn"; + static string IColumnDefinition.GetColumnHeader() => "Size"; + static string IColumnDefinition.GetColumnTemplateResourceKey() => ColumnTemplateResourceKey; +} diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithVersion.cs b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithVersion.cs new file mode 100644 index 0000000000..18994bb203 --- /dev/null +++ b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithVersion.cs @@ -0,0 +1,15 @@ +using NexusMods.App.UI.Controls; +using R3; + +namespace NexusMods.App.UI.Pages.LibraryPage; + +public interface ILibraryItemWithVersion : ILibraryItemModel, IComparable, IColumnDefinition +{ + BindableReactiveProperty Version { get; } + + int IComparable.CompareTo(ILibraryItemWithVersion? other) => string.CompareOrdinal(Version.Value, other?.Version.Value); + + public const string ColumnTemplateResourceKey = "LibraryItemVersionColumn"; + static string IColumnDefinition.GetColumnHeader() => "Version"; + static string IColumnDefinition.GetColumnTemplateResourceKey() => ColumnTemplateResourceKey; +} diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/LibraryView.axaml b/src/NexusMods.App.UI/Pages/LibraryPage/LibraryView.axaml index 063f80c8e9..c1569ca64d 100644 --- a/src/NexusMods.App.UI/Pages/LibraryPage/LibraryView.axaml +++ b/src/NexusMods.App.UI/Pages/LibraryPage/LibraryView.axaml @@ -96,6 +96,123 @@ Classes="MainListsStyling" HorizontalAlignment="Stretch"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +