diff --git a/src/Abstractions/NexusMods.Abstractions.Games/ISortableItemProviderFactory.cs b/src/Abstractions/NexusMods.Abstractions.Games/ISortableItemProviderFactory.cs
index 06cb44f089..33ad085ebd 100644
--- a/src/Abstractions/NexusMods.Abstractions.Games/ISortableItemProviderFactory.cs
+++ b/src/Abstractions/NexusMods.Abstractions.Games/ISortableItemProviderFactory.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel;
using NexusMods.Abstractions.Loadouts;
namespace NexusMods.Abstractions.Games;
@@ -17,10 +18,75 @@ public interface ISortableItemProviderFactory
///
/// Returns id of the type of the loadout
///
- Guid StaticSortOrderTypeId { get; }
+ Guid SortOrderTypeId { get; }
///
- /// Display name for this sort order type
+ /// Display name for this sort order type
///
string SortOrderName { get; }
+
+ ///
+ /// Short descriptive title for the load order, describing the override behavior of the sort order
+ ///
+ ///
+ /// "Last Loaded plugin Wins"
+ ///
+ ///
+ /// Avoid using "higher" or "lower" terms, as the index numbers can be sorted both in ascending or descending order,
+ /// making their meaning ambiguous.
+ ///
+ string OverrideInfoTitle { get; }
+
+ ///
+ /// Heading for more details load order override information
+ ///
+ ///
+ /// "Load Order for REDmods in Cyberpunk 2077 - First Loaded Wins"
+ ///
+ string OverrideInfoHeading { get; }
+
+ ///
+ /// Detailed description of the load order and its override behavior
+ ///
+ string OverrideInfoMessage { get; }
+
+ ///
+ /// Short tooltip message to explain the winning index number in the load order
+ ///
+ string WinnerIndexToolTip { get; }
+
+ ///
+ /// Header text for the index column
+ ///
+ string IndexColumnHeader { get; }
+
+ ///
+ /// Header text for the name column
+ ///
+ string NameColumnHeader { get; }
+
+ ///
+ /// Title text to display in case there are no sortable items to sort
+ ///
+ string EmptyStateMessageTitle { get; }
+
+ ///
+ /// Contents text to display in case there are no sortable items to sort
+ ///
+ string EmptyStateMessageContents { get; }
+
+ ///
+ /// Default direction (ascending/descending) in which sortIndexes should be sorted and displayed
+ ///
+ ///
+ /// Usually ascending, but could be different depending on what the community prefers and is used to
+ ///
+ ListSortDirection SortDirectionDefault { get; }
+
+ ///
+ /// Defines whether smaller or greater index numbers win in case of conflicts between items in sorting order
+ ///
+ IndexOverrideBehavior IndexOverrideBehavior { get; }
}
+
+
diff --git a/src/Abstractions/NexusMods.Abstractions.Games/IndexOverrideBehavior.cs b/src/Abstractions/NexusMods.Abstractions.Games/IndexOverrideBehavior.cs
new file mode 100644
index 0000000000..9822e17c6b
--- /dev/null
+++ b/src/Abstractions/NexusMods.Abstractions.Games/IndexOverrideBehavior.cs
@@ -0,0 +1,17 @@
+namespace NexusMods.Abstractions.Games;
+
+///
+/// Defines whether smaller or greater index numbers win in case of conflicts between items in sorting order
+///
+public enum IndexOverrideBehavior
+{
+ ///
+ /// Items with Smaller index numbers win in case of conflicts with greater index number items
+ ///
+ SmallerIndexWins,
+
+ ///
+ /// Items with Greater index numbers win in case of conflicts with smaller index number items
+ ///
+ GreaterIndexWins,
+}
diff --git a/src/Abstractions/NexusMods.Abstractions.Library.Models/LocalFile.cs b/src/Abstractions/NexusMods.Abstractions.Library.Models/LocalFile.cs
index cd47d48004..1efd0f3506 100644
--- a/src/Abstractions/NexusMods.Abstractions.Library.Models/LocalFile.cs
+++ b/src/Abstractions/NexusMods.Abstractions.Library.Models/LocalFile.cs
@@ -22,5 +22,5 @@ public partial class LocalFile : IModelDefinition
///
/// The MD5 hash value of the file.
///
- public static readonly Md5Attribute Md5 = new(Namespace, nameof(Md5)) { IsOptional = true };
+ public static readonly Md5Attribute Md5 = new(Namespace, nameof(Md5)) { IsOptional = true, IsIndexed = true };
}
diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts/Models/LoadoutItem.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts/Models/LoadoutItem.cs
index 7234c685c9..2c026fbf51 100644
--- a/src/Abstractions/NexusMods.Abstractions.Loadouts/Models/LoadoutItem.cs
+++ b/src/Abstractions/NexusMods.Abstractions.Loadouts/Models/LoadoutItem.cs
@@ -35,4 +35,13 @@ public partial class LoadoutItem : IModelDefinition
/// Optional parent of the item.
///
public static readonly ReferenceAttribute Parent = new(Namespace, nameof(Parent)) { IsIndexed = true, IsOptional = true };
+
+ [PublicAPI]
+ public partial struct ReadOnly
+ {
+ ///
+ /// True if this item contains a parent, else false.
+ ///
+ public bool HasParent() => this.Contains(LoadoutItem.Parent);
+ }
}
diff --git a/src/Examples/README.md b/src/Examples/README.md
index 209480dfe4..568249e5af 100644
--- a/src/Examples/README.md
+++ b/src/Examples/README.md
@@ -17,4 +17,4 @@ and [Using Workspaces](https://nexus-mods.github.io/NexusMods.App/development-gu
- [TreeDataGrid](./TreeDataGrid/README.md)
- [Basic TreeDataGrid](./TreeDataGrid/Basic)
- [TreeDataGrid With Single Custom Column](./TreeDataGrid/SingleColumn)
- - Further explained in [UI Coding Guidelines](../../docs/development-guidelines/UICodingGuidelines.md#trees-with-columns-treedatagrid).
+ - Further explained in [UI Coding Guidelines](../../docs/developers/development-guidelines/UICodingGuidelines.md#trees-with-columns-treedatagrid).
diff --git a/src/Examples/TreeDataGrid/README.md b/src/Examples/TreeDataGrid/README.md
index 1900fd68c3..9c4e8aa1ec 100644
--- a/src/Examples/TreeDataGrid/README.md
+++ b/src/Examples/TreeDataGrid/README.md
@@ -1,7 +1,7 @@
# TreeDataGrid Examples
Feel free to use these as a template when adding new trees.
-These are further described in [UI Coding Guidelines](../../../docs/development-guidelines/UICodingGuidelines.md#trees-with-columns-treedatagrid).
+These are further described in [UI Coding Guidelines](../../../docs/developers/development-guidelines/UICodingGuidelines.md#trees-with-columns-treedatagrid).
- [Basic TreeDataGrid](./Basic)
- This is the most basic example of a TreeDataGrid.
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/BannerlordDiagnosticEmitter.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/BannerlordDiagnosticEmitter.cs
index d120ee8921..1ae24053ff 100644
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/BannerlordDiagnosticEmitter.cs
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/BannerlordDiagnosticEmitter.cs
@@ -5,6 +5,7 @@
using NexusMods.Abstractions.Diagnostics;
using NexusMods.Abstractions.Diagnostics.Emitters;
using NexusMods.Abstractions.Loadouts;
+using NexusMods.Abstractions.Loadouts.Extensions;
using NexusMods.Abstractions.Resources;
using NexusMods.Games.MountAndBlade2Bannerlord.Models;
namespace NexusMods.Games.MountAndBlade2Bannerlord.Diagnostics;
@@ -36,7 +37,12 @@ public async IAsyncEnumerable Diagnose(Loadout.ReadOnly loadout, Can
foreach (var module in modulesAndMods)
{
var mod = module.Item1;
- var isEnabled = !mod.AsLoadoutItemGroup().AsLoadoutItem().IsDisabled;
+ var loadoutItem = mod.AsLoadoutItemGroup().AsLoadoutItem();
+
+ // Note(sewer): We create a LoadoutItemGroup for each module, which is a child of the one
+ // used for the archive. Since in theory the item can be disabled at any level
+ // in the tree, we need to check if the parent is disabled.
+ var isEnabled = loadoutItem.IsEnabled();
isEnabledDict[module.Item2] = isEnabled;
}
@@ -50,6 +56,9 @@ public async IAsyncEnumerable Diagnose(Loadout.ReadOnly loadout, Can
foreach (var moduleAndMod in isEnabledDict)
{
var moduleInfo = moduleAndMod.Key;
+ if (!moduleAndMod.Value)
+ continue;
+
// Note(sewer): All modules are valid by definition
// All modules are selected by definition.
foreach (var diagnostic in ModuleUtilities.ValidateModuleEx(modulesOnly, moduleInfo, module => isEnabledDict.ContainsKey(module), _ => true, false).Select(x => CreateDiagnostic(x)))
diff --git a/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/SortOrder/RedMod/RedModSortableItemProvider.cs b/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/SortOrder/RedMod/RedModSortableItemProvider.cs
index 650156ba5b..c3ccf19056 100644
--- a/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/SortOrder/RedMod/RedModSortableItemProvider.cs
+++ b/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/SortOrder/RedMod/RedModSortableItemProvider.cs
@@ -361,7 +361,7 @@ private async Task PersistSortableEntries(List orderList)
var newSortOrder = new Abstractions.Loadouts.SortOrder.New(ts)
{
LoadoutId = loadoutId,
- SortOrderTypeId = parentFactory.StaticSortOrderTypeId,
+ SortOrderTypeId = parentFactory.SortOrderTypeId,
};
var newRedModSortOrder = new RedModSortOrder.New(ts, newSortOrder.SortOrderId)
diff --git a/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/SortOrder/RedMod/RedModSortableItemProviderFactory.cs b/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/SortOrder/RedMod/RedModSortableItemProviderFactory.cs
index 1422729510..d0483808f3 100644
--- a/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/SortOrder/RedMod/RedModSortableItemProviderFactory.cs
+++ b/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/SortOrder/RedMod/RedModSortableItemProviderFactory.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel;
using System.Diagnostics;
using DynamicData;
using NexusMods.Abstractions.Games;
@@ -11,9 +12,34 @@ public class RedModSortableItemProviderFactory : ISortableItemProviderFactory
{
private readonly IConnection _connection;
private readonly Dictionary _providers = new();
+ private static readonly Guid StaticTypeId = new("9120C6F5-E0DD-4AD2-A99E-836F56796950");
- public Guid StaticSortOrderTypeId { get; } = new("9120C6F5-E0DD-4AD2-A99E-836F56796950");
- public string SortOrderName { get; } = "REDmod Load Order";
+ public Guid SortOrderTypeId => StaticTypeId;
+
+ public string SortOrderName => "REDmod Load Order";
+
+ public string OverrideInfoTitle => "First Loaded REDmod Wins";
+
+ public string OverrideInfoHeading => "Load Order for REDmods in Cyberpunk 2077 - First Loaded Wins";
+
+ public string OverrideInfoMessage => """
+ Some Cyberpunk 2077 mods use REDmods modules to alter core gameplay elements. If two REDmods modify the same part of the game, the one loaded first will take priority and overwrite changes from those loaded later.
+
+ For example, the 1st position overwrites the 2nd, the 2nd overwrites the 3rd, and so on.
+ """;
+
+ public string WinnerIndexToolTip => "The REDmod that will overwrite all others";
+
+ public string IndexColumnHeader => "Load Order";
+
+ public string NameColumnHeader => "REDmod Name";
+
+ public string EmptyStateMessageTitle => "No REDmods detected";
+ public string EmptyStateMessageContents => "Some mods contain REDmods modules that can alter core gameplay elements. When detected they will appear here for load order configuration.";
+
+ public ListSortDirection SortDirectionDefault => ListSortDirection.Ascending;
+
+ public IndexOverrideBehavior IndexOverrideBehavior => IndexOverrideBehavior.SmallerIndexWins;
public RedModSortableItemProviderFactory(IConnection connection)
{
@@ -56,7 +82,7 @@ public RedModSortableItemProviderFactory(IConnection connection)
Debug.Assert(false, $"RedModSortableItemProviderFactory: provider not found for loadout {removal.Current.LoadoutId}");
continue;
}
-
+
// TODO: Delete SortOrder and SortableItem entities from DB if it isn't done in Synchronizer.DeleteLoadout()
provider.Dispose();
}
@@ -74,5 +100,4 @@ public ILoadoutSortableItemProvider GetLoadoutSortableItemProvider(LoadoutId loa
throw new InvalidOperationException($"RedModSortableItemProviderFactory: provider not found for loadout {loadoutId}");
}
-
}
diff --git a/src/NexusMods.App.UI/Overlays/AOverlayViewModel.cs b/src/NexusMods.App.UI/Overlays/AOverlayViewModel.cs
index cb09abe0fb..8a254f4f66 100644
--- a/src/NexusMods.App.UI/Overlays/AOverlayViewModel.cs
+++ b/src/NexusMods.App.UI/Overlays/AOverlayViewModel.cs
@@ -34,7 +34,7 @@ public IOverlayController Controller
private readonly TaskCompletionSource _taskCompletionSource = new();
public Task CompletionTask => _taskCompletionSource.Task;
- public void Close()
+ public virtual void Close()
{
Debug.Assert(Controller != null, "Controller != null");
if (Status == Status.Closed)
diff --git a/src/NexusMods.App.UI/Overlays/Login/StartupMessageBox/LoginMessageBoxViewModel.cs b/src/NexusMods.App.UI/Overlays/Login/StartupMessageBox/LoginMessageBoxViewModel.cs
index 04f59650c2..0b990d7d3b 100644
--- a/src/NexusMods.App.UI/Overlays/Login/StartupMessageBox/LoginMessageBoxViewModel.cs
+++ b/src/NexusMods.App.UI/Overlays/Login/StartupMessageBox/LoginMessageBoxViewModel.cs
@@ -27,13 +27,19 @@ public LoginMessageBoxViewModel(ISettingsManager settingsManager, ILoginManager
CancelCommand = ReactiveCommand.Create(Close);
}
+ public override void Close()
+ {
+ _settingsManager.Update(settings => settings with { HasShownModal = true });
+ base.Close();
+ }
+
public ReactiveCommand OkCommand { get; }
public ReactiveCommand CancelCommand { get; }
public bool MaybeShow()
{
- if (_settingsManager.Get().HasShownModal) return false;
- _settingsManager.Update(settings => settings with { HasShownModal = true });
+ if (_settingsManager.Get().HasShownModal)
+ return false;
_overlayController.Enqueue(this);
return true;
diff --git a/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadDesignViewModel.cs b/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadDesignViewModel.cs
index 9c1934f9ab..1545adf44a 100644
--- a/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadDesignViewModel.cs
+++ b/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadDesignViewModel.cs
@@ -5,6 +5,7 @@
using NexusMods.App.UI.Windows;
using NexusMods.App.UI.WorkspaceSystem;
using NexusMods.Paths;
+using R3;
namespace NexusMods.App.UI.Pages.CollectionDownload;
@@ -21,7 +22,7 @@ public CollectionDownloadDesignViewModel() : base(new DesignWindowManager()) { }
public string AuthorName => "Lowtonotolerance";
public string Summary =>
- "Aims to improves vanilla gameplay while adding minimal additional content. Aims to improves vanilla gameplay while adding minimal additional content. Aims to improves vanilla gameplay while adding minimal additional content. Aims to improves vanilla gameplay while adding minimal additional content.";
+ "1.6.14 The story of Stardew Valley expands outside of Pelican Town with this expanded collection designed to stay true to the original game. Created with co-op in mind, perfect for experienced solo-players. Easy install, includes configuration.";
public string Category => "Themed";
public bool IsAdult => true;
@@ -35,4 +36,7 @@ public CollectionDownloadDesignViewModel() : base(new DesignWindowManager()) { }
public Bitmap TileImage { get; } = new(AssetLoader.Open(new Uri("avares://NexusMods.App.UI/Assets/DesignTime/collection_tile_image.png")));
public Bitmap BackgroundImage { get; } = new(AssetLoader.Open(new Uri("avares://NexusMods.App.UI/Assets/DesignTime/header-background.webp")));
public string CollectionStatusText { get; } = "0 of 9 mods downloaded";
+
+ public ReactiveCommand DownloadAllCommand { get; } = new ReactiveCommand();
+ public ReactiveCommand InstallCollectionCommand { get; } = new ReactiveCommand();
}
diff --git a/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadView.axaml b/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadView.axaml
index 1d2473f786..6f3fc347e2 100644
--- a/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadView.axaml
+++ b/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadView.axaml
@@ -9,7 +9,7 @@
xmlns:panels="clr-namespace:Avalonia.Labs.Panels;assembly=Avalonia.Labs.Panels"
xmlns:icons="clr-namespace:NexusMods.Icons;assembly=NexusMods.Icons"
xmlns:controls="clr-namespace:NexusMods.App.UI.Controls"
- mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="600"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="NexusMods.App.UI.Pages.CollectionDownload.CollectionDownloadView">
@@ -48,24 +48,29 @@
-
-
+
+
+
-
-
+
+
+
+
-
-
-
+
+
+
+
-
- COLLECTION DOWNLOAD
-
-
-
+
+
+ COLLECTION DOWNLOAD
+
+
+
@@ -73,42 +78,44 @@
-
+
-
-
+
+
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
+
+
@@ -118,32 +125,33 @@
+
-
+
Required
-
+
-
+
Optional
-
+
-
+
diff --git a/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadView.axaml.cs b/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadView.axaml.cs
index 5e7eb57dc0..6689461da3 100644
--- a/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadView.axaml.cs
+++ b/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadView.axaml.cs
@@ -20,6 +20,9 @@ public CollectionDownloadView()
this.WhenActivated(d =>
{
+ this.BindCommand(ViewModel, vm => vm.DownloadAllCommand, view => view.DownloadAllButton)
+ .DisposeWith(d);
+
this.OneWayBind(ViewModel, vm => vm.RequiredDownloadsAdapter.Source.Value, view => view.RequiredDownloadsTree.Source)
.DisposeWith(d);
@@ -95,7 +98,7 @@ public CollectionDownloadView()
_ => "LowRating",
};
})
- .Subscribe(className => OverallRating.Classes.Add(className))
+ .Subscribe(className => OverallRatingPanel.Classes.Add(className))
.DisposeWith(d);
});
}
diff --git a/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadViewModel.cs b/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadViewModel.cs
index cf6bf54232..e8583c34ca 100644
--- a/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadViewModel.cs
+++ b/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadViewModel.cs
@@ -5,6 +5,7 @@
using Microsoft.Extensions.DependencyInjection;
using NexusMods.Abstractions.Jobs;
using NexusMods.Abstractions.NexusModsLibrary.Models;
+using NexusMods.Abstractions.NexusWebApi;
using NexusMods.Abstractions.NexusWebApi.Types;
using NexusMods.Abstractions.UI.Extensions;
using NexusMods.App.UI.Controls;
@@ -19,6 +20,7 @@
using R3;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
+using ReactiveCommand = R3.ReactiveCommand;
namespace NexusMods.App.UI.Pages.CollectionDownload;
@@ -28,6 +30,7 @@ public class CollectionDownloadViewModel : APageViewModel();
_nexusModsDataProvider = serviceProvider.GetRequiredService();
_collectionDownloader = new CollectionDownloader(_serviceProvider);
@@ -75,6 +79,15 @@ public CollectionDownloadViewModel(
RequiredDownloadsCount = requiredDownloadCount;
OptionalDownloadsCount = optionalDownloadCount;
+ var loginManager = serviceProvider.GetRequiredService();
+ DownloadAllCommand = loginManager.IsPremiumObservable.ToObservable().ToReactiveCommand(
+ executeAsync: (_, cancellationToken) => _collectionDownloader.DownloadAll(_revision, onlyRequired: true, db: _connection.Db, cancellationToken: cancellationToken),
+ awaitOperation: AwaitOperation.Drop,
+ configureAwait: false
+ );
+
+ InstallCollectionCommand = new ReactiveCommand(canExecuteSource: Observable.Return(false), initialCanExecute: false);
+
this.WhenActivated(disposables =>
{
RequiredDownloadsAdapter.Activate();
@@ -97,24 +110,7 @@ public CollectionDownloadViewModel(
.Subscribe(this, static (bitmap, self) => self.AuthorAvatar = bitmap)
.AddTo(disposables);
- _nexusModsDataProvider
- .ObserveCollectionItems(revisionMetadata)
- .SubscribeWithErrorLogging()
- .AddTo(disposables);
-
- RequiredDownloadsAdapter.MessageSubject.SubscribeAwait(
- onNextAsync: (message, cancellationToken) =>
- {
- return message.Item.Match(
- f0: x => _collectionDownloader.Download(x, cancellationToken),
- f1: x => _collectionDownloader.Download(x, cancellationToken)
- );
- },
- awaitOperation: AwaitOperation.Parallel,
- configureAwait: false
- ).AddTo(disposables);
-
- OptionalDownloadsAdapter.MessageSubject.SubscribeAwait(
+ RequiredDownloadsAdapter.MessageSubject.Merge(OptionalDownloadsAdapter.MessageSubject).SubscribeAwait(
onNextAsync: (message, cancellationToken) =>
{
return message.Item.Match(
@@ -148,6 +144,9 @@ public CollectionDownloadViewModel(
[Reactive] public Bitmap? BackgroundImage { get; private set; }
[Reactive] public Bitmap? AuthorAvatar { get; private set; }
[Reactive] public string CollectionStatusText { get; private set; }
+
+ public ReactiveCommand DownloadAllCommand { get; }
+ public ReactiveCommand InstallCollectionCommand { get; }
}
public record DownloadMessage(DownloadableItem Item);
diff --git a/src/NexusMods.App.UI/Pages/CollectionDownload/ICollectionDownloadViewModel.cs b/src/NexusMods.App.UI/Pages/CollectionDownload/ICollectionDownloadViewModel.cs
index 3b95c38e3e..10310b0d52 100644
--- a/src/NexusMods.App.UI/Pages/CollectionDownload/ICollectionDownloadViewModel.cs
+++ b/src/NexusMods.App.UI/Pages/CollectionDownload/ICollectionDownloadViewModel.cs
@@ -80,4 +80,14 @@ public interface ICollectionDownloadViewModel : IPageViewModelInterface
/// Collection status text.
///
string CollectionStatusText { get; }
+
+ ///
+ /// Command to download all downloads.
+ ///
+ R3.ReactiveCommand DownloadAllCommand { get; }
+
+ ///
+ /// Command to install the collection.
+ ///
+ R3.ReactiveCommand InstallCollectionCommand { get; }
}
diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionCardDesignViewModel.cs b/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionCardDesignViewModel.cs
index 77cb67ad53..be645b6618 100644
--- a/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionCardDesignViewModel.cs
+++ b/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionCardDesignViewModel.cs
@@ -11,15 +11,15 @@ public class CollectionCardDesignViewModel : AViewModel "Vanilla+ [Quality of Life]";
public Bitmap Image => new(AssetLoader.Open(new Uri("avares://NexusMods.App.UI/Assets/DesignTime/collection_tile_image.png")));
- public string Summary => "1.6.8 This visual mod collection is designed to create a witchy medieval cottage aethetic experience for Stardew Valley, and Stardew Valley Expanded.";
+ public string Summary => "1.6.14 The story of Stardew Valley expands outside of Pelican Town with this expanded collection designed to stay true to the original game. Created with co-op in mind, perfect for experienced solo-players. Easy install, includes configuration.";
public string Category => "Themed";
public int NumDownloads => 9;
public ulong EndorsementCount => 248;
public ulong TotalDownloads => 30_000;
public Size TotalSize => Size.From(907 * 1024 * 1024);
- public Percent OverallRating => Percent.CreateClamped(0.84);
+ public Percent OverallRating => Percent.CreateClamped(0.54);
public bool IsAdult => true;
public string AuthorName => "FantasyAuthor";
public Bitmap AuthorAvatar => new(AssetLoader.Open(new Uri("avares://NexusMods.App.UI/Assets/DesignTime/avatar.webp")));
- public R3.ReactiveCommand OpenCollectionDownloadPageCommand { get; } = new(canExecuteSource: R3.Observable.Return(false), initialCanExecute: false);
+ public R3.ReactiveCommand OpenCollectionDownloadPageCommand { get; } = new(canExecuteSource: R3.Observable.Return(true), initialCanExecute: true);
}
diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionCardView.axaml b/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionCardView.axaml
index 60269863cb..62281065e0 100644
--- a/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionCardView.axaml
+++ b/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionCardView.axaml
@@ -11,22 +11,27 @@
xmlns:navigation="clr-namespace:NexusMods.App.UI.Controls.Navigation"
mc:Ignorable="d" d:DesignWidth="508" d:DesignHeight="288"
x:Class="NexusMods.App.UI.Pages.LibraryPage.Collections.CollectionCardView">
+
+
-
+
+
-
+
-
+
+
-
-
-
+
+
+
+
@@ -35,68 +40,68 @@
-
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
-
-
+
+
+
-
-
-
+
diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionCardView.axaml.cs b/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionCardView.axaml.cs
index 402cc1fdcb..38bb240e46 100644
--- a/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionCardView.axaml.cs
+++ b/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionCardView.axaml.cs
@@ -65,7 +65,7 @@ public CollectionCardView()
_ => "LowRating",
};
})
- .Subscribe(className => OverallRating.Classes.Add(className))
+ .Subscribe(className => OverallRatingPanel.Classes.Add(className))
.DisposeWith(d);
});
}
diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionsView.axaml b/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionsView.axaml
index 1764ad6286..1c8f14a9b5 100644
--- a/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionsView.axaml
+++ b/src/NexusMods.App.UI/Pages/LibraryPage/Collections/CollectionsView.axaml
@@ -5,26 +5,33 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:collections="clr-namespace:NexusMods.App.UI.Pages.LibraryPage.Collections"
+ xmlns:panels="clr-namespace:Avalonia.Labs.Panels;assembly=Avalonia.Labs.Panels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="NexusMods.App.UI.Pages.LibraryPage.Collections.CollectionsView">
+
-
+
+
diff --git a/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/ILoadOrderItemModel.cs b/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/ILoadOrderItemModel.cs
index e5098d008e..a5f8bf36cd 100644
--- a/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/ILoadOrderItemModel.cs
+++ b/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/ILoadOrderItemModel.cs
@@ -12,4 +12,7 @@ public interface ILoadOrderItemModel : ITreeDataGridItemModel MoveDown { get; }
public int SortIndex { get; }
public string DisplayName { get; }
+
+ public string ModName { get; }
+ public bool IsActive { get; }
}
diff --git a/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/ILoadOrderViewModel.cs b/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/ILoadOrderViewModel.cs
index b8ab53fec6..eff80c7a7c 100644
--- a/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/ILoadOrderViewModel.cs
+++ b/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/ILoadOrderViewModel.cs
@@ -1,10 +1,73 @@
+using System.ComponentModel;
+using System.Reactive;
using NexusMods.Abstractions.UI;
+using ReactiveUI;
namespace NexusMods.App.UI.Pages.Sorting;
public interface ILoadOrderViewModel : IViewModelInterface
{
+ ///
+ /// TreeDataGridAdapter for the Load Order, for setting up the TreeDataGrid
+ ///
+ LoadOrderTreeDataGridAdapter Adapter { get; }
+
+ ///
+ /// Name of this sort order type
+ ///
string SortOrderName { get; }
- LoadOrderTreeDataGridAdapter Adapter { get; }
+ ///
+ /// The always visible First/Last wins heading text
+ /// Also used for trophy tooltip heading
+ ///
+ string InfoAlertTitle { get; }
+
+ ///
+ /// The title of the alert message, only visible if the alert is visible
+ ///
+ string InfoAlertHeading { get; }
+
+ ///
+ /// The contents of the alert message, only visible if the alert is visible
+ ///
+ string InfoAlertMessage { get; }
+
+ ///
+ /// Whether the alert message should be visible or not
+ ///
+ bool InfoAlertIsVisible { get; set; }
+
+ ///
+ /// Command to invoke when the info alert icon is pressed (either to show or hide the alert)
+ ///
+ ReactiveCommand InfoAlertCommand { get; }
+
+ ///
+ /// Tooltip message contents for the trophy icon
+ ///
+ string TrophyToolTip { get; }
+
+ ///
+ /// The current ascending/descending direction in which the SortIndexes are sorted and displayed
+ ///
+ ListSortDirection SortDirectionCurrent { get; set; }
+
+ ///
+ /// Whether the winning item is at the top or bottom of the list
+ ///
+ ///
+ /// Depends on the way the game load order works and can't be deduced exclusively from the sort direction
+ ///
+ bool IsWinnerTop { get; }
+
+ ///
+ /// Title text for the empty state, in case there are no sortable items to display
+ ///
+ string EmptyStateMessageTitle { get; }
+
+ ///
+ /// Contents text for the empty state, in case there are no sortable items to display
+ ///
+ string EmptyStateMessageContents { get; }
}
diff --git a/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/LoadOrderItemModel.cs b/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/LoadOrderItemModel.cs
index 70893c82ee..632c02191b 100644
--- a/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/LoadOrderItemModel.cs
+++ b/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/LoadOrderItemModel.cs
@@ -13,6 +13,10 @@ public class LoadOrderItemModel : TreeDataGridItemModel MoveDown { get; }
public int SortIndex { get; }
public string DisplayName { get; }
+
+ // TODO: Populate these properly
+ public string ModName { get; } = string.Empty;
+ public bool IsActive { get; } = true;
public LoadOrderItemModel(ISortableItem sortableItem)
{
diff --git a/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/LoadOrderViewModel.cs b/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/LoadOrderViewModel.cs
index 70ba859dbe..8f1b28c34c 100644
--- a/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/LoadOrderViewModel.cs
+++ b/src/NexusMods.App.UI/Pages/Sorting/LoadOrder/LoadOrderViewModel.cs
@@ -1,3 +1,5 @@
+using System.ComponentModel;
+using System.Reactive;
using System.Reactive.Disposables;
using Avalonia.Controls.Models.TreeDataGrid;
using DynamicData;
@@ -7,20 +9,45 @@
using NexusMods.Abstractions.UI;
using NexusMods.App.UI.Controls;
using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
namespace NexusMods.App.UI.Pages.Sorting;
public class LoadOrderViewModel : AViewModel, ILoadOrderViewModel
{
public string SortOrderName { get; }
+ public string InfoAlertTitle { get; }
+ public string InfoAlertHeading { get; }
+ public string InfoAlertMessage { get; }
+ [Reactive] public bool InfoAlertIsVisible { get; set; }
+ public ReactiveCommand InfoAlertCommand { get; } = ReactiveCommand.Create(() => { });
+ public string TrophyToolTip { get; }
+ [Reactive] public ListSortDirection SortDirectionCurrent { get; set; }
+ [Reactive] public bool IsWinnerTop { get; set; }
+ public string EmptyStateMessageTitle { get; }
+ public string EmptyStateMessageContents { get; }
public LoadOrderTreeDataGridAdapter Adapter { get; }
- public LoadOrderViewModel(LoadoutId loadoutId, ISortableItemProviderFactory sortableItemProviderFactory)
+ public LoadOrderViewModel(LoadoutId loadoutId, ISortableItemProviderFactory itemProviderFactory)
{
- SortOrderName = sortableItemProviderFactory.SortOrderName;
- var provider = sortableItemProviderFactory.GetLoadoutSortableItemProvider(loadoutId);
+ var provider = itemProviderFactory.GetLoadoutSortableItemProvider(loadoutId);
+
+ SortOrderName = itemProviderFactory.SortOrderName;
+ InfoAlertTitle = itemProviderFactory.OverrideInfoTitle;
+ InfoAlertHeading = itemProviderFactory.OverrideInfoHeading;
+ InfoAlertMessage = itemProviderFactory.OverrideInfoMessage;
+ TrophyToolTip = itemProviderFactory.WinnerIndexToolTip;
+ EmptyStateMessageTitle = itemProviderFactory.EmptyStateMessageTitle;
+ EmptyStateMessageContents = itemProviderFactory.EmptyStateMessageContents;
+
+ // TODO: load these from settings
+ SortDirectionCurrent = itemProviderFactory.SortDirectionDefault;
+ IsWinnerTop = itemProviderFactory.IndexOverrideBehavior == IndexOverrideBehavior.SmallerIndexWins &&
+ SortDirectionCurrent == ListSortDirection.Ascending;
+ InfoAlertIsVisible = true;
+
Adapter = new LoadOrderTreeDataGridAdapter(provider);
Adapter.ViewHierarchical.Value = true;
@@ -56,7 +83,7 @@ protected override IColumn[] CreateColumns(bool viewHierarc
[
// TODO: Use to create the columns using interfaces
new HierarchicalExpanderColumn(
- inner: CreateIndexColumn(),
+ inner: CreateIndexColumn(_sortableItemsProvider.ParentFactory.IndexColumnHeader),
childSelector: static model => model.Children,
hasChildrenSelector: static model => model.HasChildren.Value,
isExpandedSelector: static model => model.IsExpanded
@@ -64,14 +91,14 @@ protected override IColumn[] CreateColumns(bool viewHierarc
{
Tag = "expander",
},
- CreateNameColumn(),
+ CreateNameColumn(_sortableItemsProvider.ParentFactory.NameColumnHeader),
];
}
- private static IColumn CreateIndexColumn()
+ private static IColumn CreateIndexColumn(string headerName)
{
return new CustomTemplateColumn(
- header: "Load Order",
+ header: headerName,
cellTemplateResourceKey: "LoadOrderItemIndexColumnTemplate",
options: new TemplateColumnOptions
{
@@ -84,10 +111,10 @@ private static IColumn CreateIndexColumn()
};
}
- private static IColumn CreateNameColumn()
+ private static IColumn CreateNameColumn(string headerName)
{
return new CustomTemplateColumn(
- header: "Name",
+ header: headerName,
cellTemplateResourceKey: "LoadOrderItemNameColumnTemplate",
options: new TemplateColumnOptions
{
diff --git a/src/NexusMods.Collections/CollectionDownloader.cs b/src/NexusMods.Collections/CollectionDownloader.cs
index 51338c0cd6..3dc2aa0811 100644
--- a/src/NexusMods.Collections/CollectionDownloader.cs
+++ b/src/NexusMods.Collections/CollectionDownloader.cs
@@ -1,9 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using NexusMods.Abstractions.Collections;
using NexusMods.Abstractions.Library;
+using NexusMods.Abstractions.Library.Models;
+using NexusMods.Abstractions.NexusModsLibrary;
using NexusMods.Abstractions.NexusModsLibrary.Models;
using NexusMods.Abstractions.NexusWebApi;
using NexusMods.CrossPlatform.Process;
+using NexusMods.MnemonicDB.Abstractions;
using NexusMods.Networking.NexusWebApi;
using NexusMods.Paths;
@@ -76,11 +80,7 @@ private async ValueTask CanDirectDownload(CollectionDownloadExternal.ReadO
return false;
}
}
-
- ///
- /// Downloads an external file or opens the browser if the file can't be downloaded automatically.
- ///
- public async ValueTask Download(CollectionDownloadExternal.ReadOnly download, CancellationToken cancellationToken)
+ private async ValueTask Download(CollectionDownloadExternal.ReadOnly download, bool onlyDirectDownloads, CancellationToken cancellationToken)
{
if (await CanDirectDownload(download, cancellationToken))
{
@@ -90,11 +90,46 @@ public async ValueTask Download(CollectionDownloadExternal.ReadOnly download, Ca
}
else
{
+ if (onlyDirectDownloads) return;
+
_logger.LogInformation("Unable to direct download `{Uri}`, using browse as a fallback", download.Uri);
await _osInterop.OpenUrl(download.Uri, logOutput: false, fireAndForget: true, cancellationToken: cancellationToken);
}
}
+ ///
+ /// Downloads an external file or opens the browser if the file can't be downloaded automatically.
+ ///
+ public ValueTask Download(CollectionDownloadExternal.ReadOnly download, CancellationToken cancellationToken)
+ {
+ return Download(download, onlyDirectDownloads: false, cancellationToken);
+ }
+
+ ///
+ /// Checks whether the item was already downloaded.
+ ///
+ public static bool IsDownloaded(CollectionDownloadExternal.ReadOnly download, IDb? db = null)
+ {
+ db ??= download.Db;
+ var directDownloadDatoms = db.Datoms(DirectDownloadLibraryFile.Md5, download.Md5);
+ if (directDownloadDatoms.Count > 0) return true;
+
+ var locallyAddedDatoms = db.Datoms(LocalFile.Md5, download.Md5);
+ if (locallyAddedDatoms.Count > 0) return true;
+
+ return false;
+ }
+
+ ///
+ /// Checks whether the item was already downloaded.
+ ///
+ public static bool IsDownloaded(CollectionDownloadNexusMods.ReadOnly download, IDb? db = null)
+ {
+ db ??= download.Db;
+ var datoms = db.Datoms(NexusModsLibraryItem.FileMetadata, download.FileMetadata);
+ return datoms.Count > 0;
+ }
+
///
/// Downloads a file from nexus mods for premium users or opens the download page in the browser.
///
@@ -111,4 +146,38 @@ public async ValueTask Download(CollectionDownloadNexusMods.ReadOnly download, C
await _osInterop.OpenUrl(download.FileMetadata.GetUri(), logOutput: false, fireAndForget: true, cancellationToken: cancellationToken);
}
}
+
+ ///
+ /// Downloads everything in the revision.
+ ///
+ public async ValueTask DownloadAll(
+ CollectionRevisionMetadata.ReadOnly revisionMetadata,
+ bool onlyRequired,
+ IDb? db = null,
+ int maxDegreeOfParallelism = -1,
+ CancellationToken cancellationToken = default)
+ {
+ var downloads = revisionMetadata.Downloads.ToArray();
+
+ await Parallel.ForAsync(fromInclusive: 0, toExclusive: downloads.Length, parallelOptions: new ParallelOptions
+ {
+ CancellationToken = cancellationToken,
+ MaxDegreeOfParallelism = maxDegreeOfParallelism == -1 ? Environment.ProcessorCount : maxDegreeOfParallelism,
+ }, body: async (index, token) =>
+ {
+ var download = downloads[index];
+
+ if (download.IsOptional && onlyRequired) return;
+
+ if (download.TryGetAsCollectionDownloadNexusMods(out var nexusModsDownload))
+ {
+ if (IsDownloaded(nexusModsDownload, db)) return;
+ await Download(nexusModsDownload, token);
+ } else if (download.TryGetAsCollectionDownloadExternal(out var externalDownload))
+ {
+ if (IsDownloaded(externalDownload, db)) return;
+ await Download(externalDownload, onlyDirectDownloads: true, token);
+ }
+ });
+ }
}
diff --git a/src/NexusMods.DataModel/Settings/MnemonicDBSettingsBackend.cs b/src/NexusMods.DataModel/Settings/MnemonicDBSettingsBackend.cs
index dd3e813c09..3145eb2980 100644
--- a/src/NexusMods.DataModel/Settings/MnemonicDBSettingsBackend.cs
+++ b/src/NexusMods.DataModel/Settings/MnemonicDBSettingsBackend.cs
@@ -10,7 +10,7 @@ namespace NexusMods.DataModel.Settings;
///
/// A settings backend that uses the MnemonicDB for storage.
///
-internal sealed class MnemonicDBSettingsBackend : ISettingsStorageBackend
+internal sealed class MnemonicDBSettingsBackend : IAsyncSettingsStorageBackend
{
private readonly ILogger _logger;
private readonly Lazy _conn;
@@ -58,9 +58,18 @@ public void Dispose()
return null;
}
}
+ private static string GetId() where T : ISettings
+ {
+ return typeof(T).FullName ?? typeof(T).Name;
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ return ValueTask.CompletedTask;
+ }
///
- public void Save(T value) where T : class, ISettings, new()
+ public async ValueTask Save(T value, CancellationToken cancellationToken) where T : class, ISettings, new()
{
var db = _conn.Value.Db;
using var tx = _conn.Value.BeginTransaction();
@@ -77,26 +86,15 @@ public void Dispose()
}
tx.Add(id, Setting.Value, Serialize(value) ?? "null");
- tx.Commit().ContinueWith(t =>
- {
- if (t.IsFaulted)
- _logger.LogError(t.Exception, "Failed to save settings for `{Type}`", typeof(T));
- }
- );
+ await tx.Commit();
}
///
- public T? Load() where T : class, ISettings, new()
+ public ValueTask Load(CancellationToken cancellationToken) where T : class, ISettings, new()
{
var settings = Setting.FindByName(_conn.Value.Db, GetId()).ToArray();
if (!settings.Any())
- return null;
-
- return Deserialize(settings.First().Value);
- }
-
- private static string GetId() where T : ISettings
- {
- return typeof(T).FullName ?? typeof(T).Name;
+ return ValueTask.FromResult(null);
+ return ValueTask.FromResult(Deserialize(settings.First().Value));
}
}
diff --git a/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/Controls/TabControl/TabControlStyles.axaml b/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/Controls/TabControl/TabControlStyles.axaml
new file mode 100644
index 0000000000..2e1b890b72
--- /dev/null
+++ b/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/Controls/TabControl/TabControlStyles.axaml
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/StylesIndex.axaml b/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/StylesIndex.axaml
index 5ed4749b3f..faa360cc0d 100644
--- a/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/StylesIndex.axaml
+++ b/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/StylesIndex.axaml
@@ -29,6 +29,7 @@
+
diff --git a/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/UserControls/CollectionCards/CollectionCardStyles.axaml b/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/UserControls/CollectionCards/CollectionCardStyles.axaml
index e933744cc4..81e9fee86f 100644
--- a/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/UserControls/CollectionCards/CollectionCardStyles.axaml
+++ b/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/UserControls/CollectionCards/CollectionCardStyles.axaml
@@ -5,12 +5,13 @@
xmlns:collections="clr-namespace:NexusMods.App.UI.Pages.LibraryPage.Collections;assembly=NexusMods.App.UI">
-
+
+ Margin="24">
@@ -20,11 +21,12 @@
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
+
+
+
+
-
+
-
-
+
-
+
-
-
-
-
-
diff --git a/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/UserControls/CollectionDownloadPage/CollectionDownloadPageStyles.axaml b/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/UserControls/CollectionDownloadPage/CollectionDownloadPageStyles.axaml
index bfae2e28e1..60d41fbf2e 100644
--- a/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/UserControls/CollectionDownloadPage/CollectionDownloadPageStyles.axaml
+++ b/src/Themes/NexusMods.Themes.NexusFluentDark/Styles/UserControls/CollectionDownloadPage/CollectionDownloadPageStyles.axaml
@@ -4,12 +4,12 @@
xmlns:collections="clr-namespace:NexusMods.App.UI.Pages.CollectionDownload;assembly=NexusMods.App.UI"
xmlns:icons="clr-namespace:NexusMods.Icons;assembly=NexusMods.Icons">
-
-
+
+
-
+
-
+
-
+
+
+
@@ -147,38 +144,55 @@
-
-
-
-
+
+
+
-
-
+
-
+
+
+
+
@@ -209,108 +223,9 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/tests/NexusMods.DataModel.SchemaVersions.Tests/LegacyDatabaseSupportTests.TestDatabase_name=SDV.4_11_2024.rocksdb.zip.verified.txt b/tests/NexusMods.DataModel.SchemaVersions.Tests/LegacyDatabaseSupportTests.TestDatabase_name=SDV.4_11_2024.rocksdb.zip.verified.txt
index b4cf6efd36..ea8591bee0 100644
--- a/tests/NexusMods.DataModel.SchemaVersions.Tests/LegacyDatabaseSupportTests.TestDatabase_name=SDV.4_11_2024.rocksdb.zip.verified.txt
+++ b/tests/NexusMods.DataModel.SchemaVersions.Tests/LegacyDatabaseSupportTests.TestDatabase_name=SDV.4_11_2024.rocksdb.zip.verified.txt
@@ -1,7 +1,7 @@
{
Name: SDV.4_11_2024.rocksdb.zip,
OldFingerprint: None,
- NewFingerprint: 0x4CF7A613C8EC8048,
+ NewFingerprint: 0xDDB40DA5E2BBCE23,
LoadoutItemGroups: 201,
Files: 16729,
Created: 2024-11-04 17:45:27
diff --git a/tests/NexusMods.DataModel.SchemaVersions.Tests/Schema.verified.md b/tests/NexusMods.DataModel.SchemaVersions.Tests/Schema.verified.md
index 9b1504d1b3..f2366d5c00 100644
--- a/tests/NexusMods.DataModel.SchemaVersions.Tests/Schema.verified.md
+++ b/tests/NexusMods.DataModel.SchemaVersions.Tests/Schema.verified.md
@@ -3,7 +3,7 @@ This schema is written to a markdown file for both documentation and validation
models in the app, then validate the tests to update this file.
## Statistics
- - Fingerprint: 0xD83A4542F4337FDB
+ - Fingerprint: 0x1A0740A80D5D60A9
- Total attributes: 143
- Total namespaces: 58
@@ -35,7 +35,7 @@ models in the app, then validate the tests to update this file.
| NexusMods.Library.LibraryFile/Hash | UInt64 | True | False | False |
| NexusMods.Library.LibraryFile/Size | UInt64 | False | False | False |
| NexusMods.Library.LibraryItem/Name | Utf8 | False | False | False |
-| NexusMods.Library.LocalFile/Md5 | UInt128 | False | False | False |
+| NexusMods.Library.LocalFile/Md5 | UInt128 | True | False | False |
| NexusMods.Library.LocalFile/OriginalPath | Utf8 | False | False | False |
| NexusMods.Loadouts.CollectionGroup/IsReadOnly | UInt8 | True | False | False |
| NexusMods.Loadouts.DeletedFile/Reason | Utf8 | False | False | False |