Skip to content

View Integration for View Loadout Group Files #3171

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

Merged
merged 49 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
49bbd2b
Added: Boilerplate for ViewLoadoutGroupView
Sewer56 May 1, 2025
c0b5040
Moved: Columns for NameWithFileIcon and FileCount to SharedColumns si…
Sewer56 May 1, 2025
a2faf85
WIP: Add the column XAML definitions for View Loadout Groups to Share…
Sewer56 May 1, 2025
197f3ab
Added: TreeDataGrid to new ViewLoadoutGroupFiles Page
Sewer56 May 2, 2025
d4a5e26
Fixed: Duplicate registrations
Sewer56 May 6, 2025
7323ed6
Fixed: There should be 2 roots in this test, nyot~ one
Sewer56 May 6, 2025
798467d
Added: Wired up the view for View Loadout Group Files
Sewer56 May 6, 2025
99f8bee
Moved: The styles to SharedStyles.axaml
Sewer56 May 1, 2025
2cc066b
Fixed: Exception on LoadoutGroupFilesProvider
Sewer56 May 6, 2025
2c6422f
Added: Missing JSON Serialization for new View Mod Files & Missing Gr…
Sewer56 May 6, 2025
a85173d
Added: Activate FileTreeAdapter and Cleanup Filter
Sewer56 May 7, 2025
9c699e5
Improved: Dispose behaviour on ViewLoadoutGroupFilesViewModel
Sewer56 May 7, 2025
3051703
WIP: As a debugging measure, let me read matchesAnyId easier
Sewer56 May 7, 2025
ec03bc3
WIP: Hack to get the ball rolling, with a filter that is not reactive…
Sewer56 May 7, 2025
f8dadb9
Fixed: FileSizeObservable now uses a size component
Sewer56 May 7, 2025
0e50fba
Removed: Redundant comment in ViewLoadoutGroupFilesTreeDataGridAdapte…
Sewer56 May 7, 2025
ff85a10
Added: Missing TreeDataGrid Source Bind
Sewer56 May 7, 2025
5f4e5c4
Updated: Use UInt32 to for the file count component where it should h…
Sewer56 May 7, 2025
47998cb
Fixed: Incorrect use of useFullFilePaths in ToModFileItemModel for Lo…
Sewer56 May 7, 2025
c646db0
Changed: Style changes based on PR Feedback
Sewer56 May 7, 2025
e1d53ab
Changed: Reuse _observableRoots based on PR feedback
Sewer56 May 7, 2025
67d2a73
Added: Additional cleanup to ViewLoadoutGroupFilesViewModel
Sewer56 May 7, 2025
f2663a5
Improved: Disposal behaviour of ViewLoadoutGroupFilesViewModel to rel…
Sewer56 May 7, 2025
8eeddd2
Improved: Dispose the Folder Generator with `ObserveModFiles` observa…
Sewer56 May 7, 2025
9bc6da9
Changed: Moved TreeFolderGeneratorCompositeItemModelAdapter per style…
Sewer56 May 7, 2025
0e960af
Revert "Removed: No longer valid search by path code."
Sewer56 May 8, 2025
eb53647
Rework: Use GamePath for Tree Folder Generator
Sewer56 May 8, 2025
cb5eaba
Misc Cleanup of TreeFolderGeneratorTests
Sewer56 May 8, 2025
42770f3
Fixed: Size column in GamePath over Tree Folder Generator
Sewer56 May 9, 2025
2eaf22e
Fixed: OpenEditor in View Loadout Groups is flipped around
Sewer56 May 9, 2025
ed8fe8c
Changed: Unselect last item after deletion in ViewLoadoutGroupFilesVM
Sewer56 May 9, 2025
4bf66b8
Merge remote-tracking branch 'origin/main' into view-for-viewloadoutg…
Sewer56 May 12, 2025
b5d4257
Update comments
Al12rs May 13, 2025
9649db2
Dispose of the adapt subscription
Al12rs May 13, 2025
654b35e
Update selection on selection change, not on selection count change
Al12rs May 13, 2025
4bf6e6e
Refactor FolderGenerator to use GamePath as key for folders, same as …
Al12rs May 13, 2025
03b9029
Refactor GetOrCreateFolder method to remove unused out parameters for…
Al12rs May 13, 2025
b8bc1a8
Avoid disposing the models from the TreeGenerator, as they are dispos…
Al12rs May 13, 2025
d0e75ce
Remove commented out code
Al12rs May 14, 2025
b5a5d3a
Add comments to the data templates
Al12rs May 14, 2025
4f99263
Move folder model initialization code to LoadoutGroupFilesProvider.cs…
Al12rs May 14, 2025
dff8c4c
Move utility method for getting the correct group for View Mod Files …
Al12rs May 14, 2025
bb8c8ab
Fix "View Mod Files" button being unavailable if a mod was empty
Al12rs May 14, 2025
4f13838
Move utility method to new view model rather than the old one
Al12rs May 14, 2025
86b958b
Move the treeDataGridAdapter to the ViewLoadoutGroupFilesViewModel fo…
Al12rs May 14, 2025
00a873b
Remove old views and view models for the View Mod Contents view
Al12rs May 14, 2025
d7a18ee
Move and rename LoadoutGroupFilesPage related files to follow the sam…
Al12rs May 14, 2025
a5dbe06
Rename function
Al12rs May 14, 2025
f8ae244
Fix folders not showing expanded icon when expanded
Al12rs May 14, 2025
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
5 changes: 5 additions & 0 deletions src/NexusMods.Abstractions.GameLocators/GamePath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ public int CompareTo(GamePath other)
/// is allowed to be a temporary id and will be replaced with the actual id the value is transacted
/// </summary>
public (EntityId, LocationId, RelativePath) ToGamePathParentTuple(EntityId id) => (id, LocationId, Path);

/// <summary>
/// Gives you an empty path.
/// </summary>
public static GamePath Empty(LocationId locationId) => new(locationId, RelativePath.Empty);
}

/// <summary>
Expand Down
64 changes: 63 additions & 1 deletion src/NexusMods.App.UI/Controls/TreeDataGrid/SharedColumns.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using JetBrains.Annotations;
using NexusMods.App.UI.Extensions;
using NexusMods.App.UI.Pages.ItemContentsFileTree.New;

namespace NexusMods.App.UI.Controls;

Expand Down Expand Up @@ -57,4 +56,67 @@ public static int Compare<TKey>(CompositeItemModel<TKey> a, CompositeItemModel<T
public static string GetColumnHeader() => "Size";
public static string GetColumnTemplateResourceKey() => ColumnTemplateResourceKey;
}

/// <summary>
/// Represents a file or folder name, accompanied by a file or folder icon.
/// </summary>
[UsedImplicitly]
public sealed class NameWithFileIcon : ICompositeColumnDefinition<NameWithFileIcon>
{
public static int Compare<TKey>(CompositeItemModel<TKey> a, CompositeItemModel<TKey> b) where TKey : notnull
{
var aValue = a.GetOptional<StringComponent>(StringComponentKey);
var bValue = b.GetOptional<StringComponent>(StringComponentKey);
return aValue.Compare(bValue);
}

public const string ColumnTemplateResourceKey = Prefix + "NameWithFileIcon";
public static readonly ComponentKey StringComponentKey = ComponentKey.From(ColumnTemplateResourceKey + "_" + nameof(StringComponent));
public static readonly ComponentKey IconComponentKey = ComponentKey.From(ColumnTemplateResourceKey + "_" + nameof(UnifiedIconComponent));

public static string GetColumnHeader() => "Name";
public static string GetColumnTemplateResourceKey() => ColumnTemplateResourceKey;
}

/// <summary>
/// Represents a count of files that lives under a folder.
/// </summary>
[UsedImplicitly]
public sealed class FileCount : ICompositeColumnDefinition<FileCount>
{
public static int Compare<TKey>(CompositeItemModel<TKey> a, CompositeItemModel<TKey> b) where TKey : notnull
{
var aValue = a.GetOptional<UInt32Component>(ComponentKey);
var bValue = b.GetOptional<UInt32Component>(ComponentKey);
// Assuming ValueComponent has a comparable Value property or implements IComparable
// Adjust the comparison logic if ValueComponent comparison needs specific handling
return aValue.Compare(bValue);
}

public const string ColumnTemplateResourceKey = Prefix + nameof(FileCount);
public static readonly ComponentKey ComponentKey = ComponentKey.From(ColumnTemplateResourceKey + "_" + "FileCount");

public static string GetColumnHeader() => "File Count";
public static string GetColumnTemplateResourceKey() => ColumnTemplateResourceKey;
}

/// <summary>
/// A variant of <see cref="ItemSize"/> used with <see cref="CompositeItemModel{GamePath}"/> as opposed to
/// <see cref="CompositeItemModel{EntityId}"/>
/// </summary>
[UsedImplicitly]
public sealed class ItemSizeOverGamePath : ICompositeColumnDefinition<ItemSizeOverGamePath>
{
public static int Compare<TKey>(CompositeItemModel<TKey> a, CompositeItemModel<TKey> b) where TKey : notnull
{
var aValue = a.GetOptional<SizeComponent>(key: ComponentKey);
var bValue = b.GetOptional<SizeComponent>(key: ComponentKey);
return aValue.Compare(bValue);
}

public const string ColumnTemplateResourceKey = Prefix + "_GamePath_" + nameof(ItemSize);
public static readonly ComponentKey ComponentKey = ComponentKey.From(ColumnTemplateResourceKey + "_GamePath_" + nameof(SizeComponent));
public static string GetColumnHeader() => "Size";
public static string GetColumnTemplateResourceKey() => ColumnTemplateResourceKey;
}
}
18 changes: 18 additions & 0 deletions src/NexusMods.App.UI/Controls/TreeDataGrid/UInt32Component.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using R3;
namespace NexusMods.App.UI.Controls;

/// <summary>
/// Component based on <see cref="uint"/>; for XAML binding convenience.
/// </summary>
public class UInt32Component : AValueComponent<uint>, IItemModelComponent<UInt32Component>, IComparable<UInt32Component>
{
public UInt32Component(uint initialValue, IObservable<uint> valueObservable, bool subscribeWhenCreated = false, bool observeOutsideUiThread = false) : base(initialValue, valueObservable, subscribeWhenCreated,
observeOutsideUiThread
) { }
public UInt32Component(uint initialValue, Observable<uint> valueObservable, bool subscribeWhenCreated = false, bool observeOutsideUiThread = false) : base(initialValue, valueObservable, subscribeWhenCreated,
observeOutsideUiThread
) { }
public UInt32Component(uint value) : base(value) { }
/// <inheritdoc />
public int CompareTo(UInt32Component? other) => Value.Value.CompareTo(other?.Value.Value ?? 0);
}

This file was deleted.

45 changes: 45 additions & 0 deletions src/NexusMods.App.UI/Helpers/DisposableObservableWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace NexusMods.App.UI.Helpers;

/// <summary>
/// A wrapper that ties the lifetime of an external object with an <see cref="IObservable{T}"/>.
/// This allows an external resource, such as a 'wrapper' or 'adapter' to be disposed when the wrapped observable is disposed.
/// </summary>
/// <remarks>
/// Use this if you want to use an adapter or wrapper around an observable and want to dispose the adapter/wrapper with it.
///
/// <code>
/// // Where you normally would return 'observable' but want to wrap it.
/// var adapter = new SomeAdapter(_connection, observable);
/// return new DisposableObservableWrapper(adapter.WrappedObservable(), adapter);
/// </code>
/// </remarks>
/// <typeparam name="T">The type of the elements in the sequence.</typeparam>
public class DisposableObservableWrapper<T>(IObservable<T> source, IDisposable additionalResource) : IObservable<T>, IDisposable
{
private readonly IObservable<T> _source = source ?? throw new ArgumentNullException(nameof(source));
private readonly IDisposable _additionalResource = additionalResource ?? throw new ArgumentNullException(nameof(additionalResource));
private bool _disposed;

/// <inheritdoc />
public IDisposable Subscribe(IObserver<T> observer)
{
ObjectDisposedException.ThrowIf(_disposed, nameof(DisposableObservableWrapper<T>));
return _source.Subscribe(observer);
}

/// <inheritdoc />
public void Dispose()
{
if (_disposed)
return;

_disposed = true;

// Dispose the additional resource when this wrapper is disposed
_additionalResource.Dispose();

// If the source is also IDisposable, dispose it too
if (_source is IDisposable disposableSource)
disposableSource.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using NexusMods.Abstractions.GameLocators;
using NexusMods.Abstractions.Loadouts;
using NexusMods.MnemonicDB.Abstractions;
namespace NexusMods.App.UI.Helpers.TreeDataGrid.New.FolderGenerator;

/// <summary>
/// Adapter for <see cref="LoadoutItem.ReadOnly"/> for <see cref="ITreeItemWithPath"/>.
/// </summary>
public readonly struct GamePathTreeItemWithPath : ITreeItemWithPath
{
private readonly GamePath _path;

/// <summary/>
public GamePathTreeItemWithPath(GamePath item) => _path = item;

/// <inheritdoc />
public GamePath GetPath() => _path;

/// <summary/>
public static implicit operator GamePathTreeItemWithPath(GamePath path) => new(path);
}

/// <summary>
/// A factory for creating <see cref="GamePathTreeItemWithPath"/> from <see cref="EntityId"/>.
/// And a database connection.
/// </summary>
public readonly struct GamePathTreeItemWithPathFactory : ITreeItemWithPathFactory<GamePath, GamePathTreeItemWithPath>
{
public GamePathTreeItemWithPath CreateItem(GamePath key) => new(key);
}
Loading
Loading