Skip to content

Commit 493e940

Browse files
erri120insomnious
andauthored
Library: Use new columns (#2151)
* Use new columns * Use better default text * fix for textblock vertical alignment --------- Co-authored-by: insomnious <[email protected]>
1 parent 0e1cd76 commit 493e940

24 files changed

+734
-479
lines changed

src/NexusMods.App.UI/Controls/TreeDataGrid/TreeDataGridAdapter.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace NexusMods.App.UI.Controls;
1212
/// Adapter class for working with <see cref="TreeDataGrid"/>.
1313
/// </summary>
1414
public abstract class TreeDataGridAdapter<TModel, TKey> : ReactiveR3Object
15-
where TModel : TreeDataGridItemModel<TModel, TKey>
15+
where TModel : class, ITreeDataGridItemModel<TModel, TKey>
1616
where TKey : notnull
1717
{
1818
public Subject<(TModel model, bool isActivating)> ModelActivationSubject { get; } = new();

src/NexusMods.App.UI/Controls/TreeDataGrid/TreeDataGridItemModel.cs

+34-19
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,51 @@
1010
namespace NexusMods.App.UI.Controls;
1111

1212
[PublicAPI]
13-
public interface ITreeDataGridItemModel : IReactiveR3Object;
13+
public interface ITreeDataGridItemModel : IReactiveR3Object
14+
{
15+
ReactiveProperty<bool> IsSelected { get; }
16+
}
1417

1518
/// <summary>
1619
/// Base class for models of <see cref="Avalonia.Controls.TreeDataGrid"/> items.
1720
/// </summary>
18-
public class TreeDataGridItemModel : ReactiveR3Object, ITreeDataGridItemModel;
21+
public class TreeDataGridItemModel : ReactiveR3Object, ITreeDataGridItemModel
22+
{
23+
public ReactiveProperty<bool> IsSelected { get; } = new(value: false);
24+
}
25+
26+
public interface ITreeDataGridItemModel<out TModel, TKey> : ITreeDataGridItemModel
27+
where TModel : class, ITreeDataGridItemModel<TModel, TKey>
28+
where TKey : notnull
29+
{
30+
BindableReactiveProperty<bool> HasChildren { get; }
31+
32+
IEnumerable<TModel> Children { get; }
33+
34+
bool IsExpanded { get; [UsedImplicitly] set; }
35+
36+
public static HierarchicalExpanderColumn<TModel> CreateExpanderColumn(IColumn<TModel> innerColumn)
37+
{
38+
return new HierarchicalExpanderColumn<TModel>(
39+
inner: innerColumn,
40+
childSelector: static model => model.Children,
41+
hasChildrenSelector: static model => model.HasChildren.Value,
42+
isExpandedSelector: static model => model.IsExpanded
43+
)
44+
{
45+
Tag = "expander",
46+
};
47+
}
48+
}
1949

2050
/// <summary>
2151
/// Generic variant of <see cref="TreeDataGridItemModel"/>.
2252
/// </summary>
2353
[PublicAPI]
24-
public class TreeDataGridItemModel<TModel, TKey> : TreeDataGridItemModel
25-
where TModel : TreeDataGridItemModel<TModel, TKey>
54+
public class TreeDataGridItemModel<TModel, TKey> : TreeDataGridItemModel, ITreeDataGridItemModel<TModel, TKey>
55+
where TModel : class, ITreeDataGridItemModel<TModel, TKey>
2656
where TKey : notnull
2757
{
28-
public ReactiveProperty<bool> IsSelected { get; } = new(value: false);
29-
3058
public IObservable<bool> HasChildrenObservable { get; init; } = Observable.Return(false);
3159
public BindableReactiveProperty<bool> HasChildren { get; } = new();
3260

@@ -152,17 +180,4 @@ [MustDisposeResource] protected static IDisposable WhenModelActivated<TItemModel
152180
{
153181
return model.WhenActivated(block);
154182
}
155-
156-
public static HierarchicalExpanderColumn<TModel> CreateExpanderColumn(IColumn<TModel> innerColumn)
157-
{
158-
return new HierarchicalExpanderColumn<TModel>(
159-
inner: innerColumn,
160-
childSelector: static model => model.Children,
161-
hasChildrenSelector: static model => model.HasChildren.Value,
162-
isExpandedSelector: static model => model.IsExpanded
163-
)
164-
{
165-
Tag = "expander",
166-
};
167-
}
168183
}

src/NexusMods.App.UI/Controls/TreeDataGrid/TreeDataGridViewHelper.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public static void SetupTreeDataGridAdapter<TView, TViewModel, TItemModel, TKey>
1616
Func<TViewModel, TreeDataGridAdapter<TItemModel, TKey>> getAdapter)
1717
where TView : ReactiveUserControl<TViewModel>
1818
where TViewModel : class, IViewModelInterface
19-
where TItemModel : TreeDataGridItemModel<TItemModel, TKey>
19+
where TItemModel : class, ITreeDataGridItemModel<TItemModel, TKey>
2020
where TKey : notnull
2121
{
2222
treeDataGrid.ElementFactory = new CustomElementFactory();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Globalization;
2+
using Humanizer;
3+
using Humanizer.Bytes;
4+
using NexusMods.Paths;
5+
using R3;
6+
7+
namespace NexusMods.App.UI.Extensions;
8+
9+
public static class FormatExtensions
10+
{
11+
public static string FormatDate(this DateTimeOffset date, DateTimeOffset now)
12+
{
13+
if (date == DateTimeOffset.MinValue || date == DateTimeOffset.MaxValue || date == DateTimeOffset.UnixEpoch) return "-";
14+
return date.Humanize(dateToCompareAgainst: now, culture: CultureInfo.CurrentUICulture);
15+
}
16+
17+
public static BindableReactiveProperty<string> ToFormattedProperty(this Observable<DateTimeOffset> source)
18+
{
19+
return source
20+
.Select(static date => date.FormatDate(now: TimeProvider.System.GetLocalNow()))
21+
.ToBindableReactiveProperty(initialValue: "");
22+
}
23+
24+
public static BindableReactiveProperty<string> ToFormattedProperty(this Observable<Size> source)
25+
{
26+
return source
27+
.Select(static size => ByteSize.FromBytes(size.Value).Humanize())
28+
.ToBindableReactiveProperty(initialValue: "");
29+
}
30+
}

src/NexusMods.App.UI/Extensions/R3Extensions.cs

+20-3
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,40 @@ [MustDisposeResource] public static IDisposable WhenActivated<T>(
1616
this T obj,
1717
Action<T, CompositeDisposable> block)
1818
where T : IReactiveR3Object
19+
{
20+
return WhenActivated(obj, state: block, static (obj, block, disposables) =>
21+
{
22+
block(obj, disposables);
23+
});
24+
}
25+
26+
/// <summary>
27+
/// Provides an activation block for <see cref="ReactiveR3Object"/>.
28+
/// </summary>
29+
[MustDisposeResource]
30+
public static IDisposable WhenActivated<T, TState>(
31+
this T obj,
32+
TState state,
33+
Action<T, TState, CompositeDisposable> block)
34+
where T : IReactiveR3Object
35+
where TState : notnull
1936
{
2037
var d = Disposable.CreateBuilder();
2138

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

25-
obj.Activation.DistinctUntilChanged().Subscribe((obj, serialDisposable, block), onNext: static (isActivated, state) =>
42+
obj.Activation.DistinctUntilChanged().Subscribe(((obj, state), serialDisposable, block), onNext: static (isActivated, state) =>
2643
{
27-
var (model, serialDisposable, block) = state;
44+
var (wrapper, serialDisposable, block) = state;
2845

2946
serialDisposable.Disposable = null;
3047
if (isActivated)
3148
{
3249
var compositeDisposable = new CompositeDisposable();
3350
serialDisposable.Disposable = compositeDisposable;
3451

35-
block(model, compositeDisposable);
52+
block(wrapper.obj, wrapper.state, compositeDisposable);
3653
}
3754
}, onCompleted: static (_, state) =>
3855
{

src/NexusMods.App.UI/NexusMods.App.UI.csproj

-6
Original file line numberDiff line numberDiff line change
@@ -596,12 +596,6 @@
596596
<Compile Update="Pages\LibraryPage\LibraryViewModel.cs">
597597
<DependentUpon>ILibraryViewModel.cs</DependentUpon>
598598
</Compile>
599-
<Compile Update="Pages\LibraryPage\FakeParentLibraryItemModel.cs">
600-
<DependentUpon>LibraryItemModel.cs</DependentUpon>
601-
</Compile>
602-
<Compile Update="Pages\LibraryPage\NexusModsModPageLibraryItemModel.cs">
603-
<DependentUpon>LibraryItemModel.cs</DependentUpon>
604-
</Compile>
605599
<Compile Update="Pages\LoadoutPage\FakeParentLoadoutItemModel.cs">
606600
<DependentUpon>LoadoutItemModel.cs</DependentUpon>
607601
</Compile>

src/NexusMods.App.UI/Pages/ILibraryDataProvider.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ namespace NexusMods.App.UI.Pages;
1212

1313
public interface ILibraryDataProvider
1414
{
15-
IObservable<IChangeSet<LibraryItemModel, EntityId>> ObserveFlatLibraryItems(LibraryFilter libraryFilter);
15+
IObservable<IChangeSet<ILibraryItemModel, EntityId>> ObserveFlatLibraryItems(LibraryFilter libraryFilter);
1616

17-
IObservable<IChangeSet<LibraryItemModel, EntityId>> ObserveNestedLibraryItems(LibraryFilter libraryFilter);
17+
IObservable<IChangeSet<ILibraryItemModel, EntityId>> ObserveNestedLibraryItems(LibraryFilter libraryFilter);
1818
}
1919

2020
public class LibraryFilter

src/NexusMods.App.UI/Pages/LibraryPage/FakeParentLibraryItemModel.cs

-83
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,79 @@
1+
using System.Diagnostics;
2+
using System.Diagnostics.CodeAnalysis;
3+
using DynamicData;
4+
using JetBrains.Annotations;
5+
using NexusMods.Abstractions.Library.Models;
6+
using NexusMods.Abstractions.Loadouts;
7+
using NexusMods.Abstractions.MnemonicDB.Attributes.Extensions;
18
using NexusMods.App.UI.Controls;
9+
using NexusMods.App.UI.Extensions;
10+
using NexusMods.MnemonicDB.Abstractions;
11+
using ObservableCollections;
12+
using R3;
213

314
namespace NexusMods.App.UI.Pages.LibraryPage;
415

5-
public interface ILibraryItemModel : ITreeDataGridItemModel;
16+
public interface ILibraryItemModel : ITreeDataGridItemModel<ILibraryItemModel, EntityId>;
17+
18+
public interface IHasTicker
19+
{
20+
Observable<DateTimeOffset>? Ticker { get; set; }
21+
}
22+
23+
public interface IHasLinkedLoadoutItems
24+
{
25+
IObservable<IChangeSet<LibraryLinkedLoadoutItem.ReadOnly, EntityId>> LinkedLoadoutItemsObservable { get; }
26+
ObservableDictionary<EntityId, LibraryLinkedLoadoutItem.ReadOnly> LinkedLoadoutItems { get; }
27+
28+
[MustDisposeResource] static IDisposable SetupLinkedLoadoutItems<TModel>(TModel self, SerialDisposable serialDisposable)
29+
where TModel : IHasLinkedLoadoutItems, ILibraryItemWithInstallAction, ILibraryItemWithInstalledDate
30+
{
31+
var disposable = self.LinkedLoadoutItems
32+
.ObserveCountChanged(notifyCurrentCount: true)
33+
.Subscribe(self, static (count, self) =>
34+
{
35+
var isInstalled = count > 0;
36+
self.IsInstalled.Value = isInstalled;
37+
self.InstallButtonText.Value = ILibraryItemWithInstallAction.GetButtonText(isInstalled);
38+
self.InstalledDate.Value = isInstalled ? self.LinkedLoadoutItems.Select(static kv => kv.Value.GetCreatedAt()).Max() : DateTimeOffset.MinValue;
39+
});
40+
41+
if (serialDisposable.Disposable is null)
42+
{
43+
serialDisposable.Disposable = self.LinkedLoadoutItemsObservable.OnUI().SubscribeWithErrorLogging(changes => self.LinkedLoadoutItems.ApplyChanges(changes));
44+
}
45+
46+
return disposable;
47+
}
48+
}
49+
50+
public interface IIsParentLibraryItemModel : ILibraryItemModel
51+
{
52+
IReadOnlyList<LibraryItemId> LibraryItemIds { get; }
53+
}
54+
55+
public interface IIsChildLibraryItemModel : ILibraryItemModel
56+
{
57+
LibraryItemId LibraryItemId { get; }
58+
}
59+
60+
[SuppressMessage("ReSharper", "PossibleInterfaceMemberAmbiguity")]
61+
public interface ILibraryItemWithDates : IHasTicker, ILibraryItemWithDownloadedDate, ILibraryItemWithInstalledDate
62+
{
63+
[MustDisposeResource]
64+
static IDisposable SetupDates<TModel>(TModel self) where TModel : class, ILibraryItemWithDates
65+
{
66+
return self.WhenActivated(static (self, disposables) =>
67+
{
68+
Debug.Assert(self.Ticker is not null, "should've been set before activation");
69+
self.Ticker.Subscribe(self, static (now, self) =>
70+
{
71+
ILibraryItemWithDownloadedDate.FormatDate(self, now: now);
72+
ILibraryItemWithInstalledDate.FormatDate(self, now: now);
73+
}).AddTo(disposables);
74+
75+
ILibraryItemWithDownloadedDate.FormatDate(self, now: TimeProvider.System.GetLocalNow());
76+
ILibraryItemWithInstalledDate.FormatDate(self, now: TimeProvider.System.GetLocalNow());
77+
});
78+
}
79+
}

0 commit comments

Comments
 (0)