diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/NexusModsLibrary.Collections.cs b/src/Networking/NexusMods.Networking.NexusWebApi/NexusModsLibrary.Collections.cs index 679dcd0454..17be315a30 100644 --- a/src/Networking/NexusMods.Networking.NexusWebApi/NexusModsLibrary.Collections.cs +++ b/src/Networking/NexusMods.Networking.NexusWebApi/NexusModsLibrary.Collections.cs @@ -44,7 +44,7 @@ public partial class NexusModsLibrary ); var collectionRevisionInfo = apiResult.Data?.CollectionRevision; - if (collectionRevisionInfo is null) throw new NotSupportedException($"API call returned no data for `{slug}` `{revisionNumber}`"); + if (collectionRevisionInfo is null) throw new NotSupportedException($"API call returned no data for collection slug `{slug}` revision `{revisionNumber}`"); using var tx = _connection.BeginTransaction(); var db = _connection.Db; diff --git a/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadViewModel.cs b/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadViewModel.cs index 0c03518b95..dde316208c 100644 --- a/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadViewModel.cs +++ b/src/NexusMods.App.UI/Pages/CollectionDownload/CollectionDownloadViewModel.cs @@ -9,8 +9,6 @@ using NexusMods.Abstractions.NexusModsLibrary.Models; using NexusMods.Abstractions.NexusWebApi; using NexusMods.Abstractions.NexusWebApi.Types; -using NexusMods.Abstractions.Resources; -using NexusMods.Abstractions.Telemetry; using NexusMods.App.UI.Controls; using NexusMods.App.UI.Extensions; using NexusMods.App.UI.Pages.LibraryPage; @@ -96,13 +94,25 @@ public CollectionDownloadViewModel( .AddTo(disposables); TreeDataGridAdapter.MessageSubject.SubscribeAwait( - onNextAsync: (message, cancellationToken) => DownloadOrOpenPage(message.Item.AsT0, cancellationToken), + onNextAsync: (message, cancellationToken) => + { + return message.Item.Match( + f0: x => DownloadOrOpenPage(x, cancellationToken), + f1: x => DownloadExternalItem(x, cancellationToken) + ); + }, awaitOperation: AwaitOperation.Parallel, configureAwait: false ).AddTo(disposables); }); } + private async ValueTask DownloadExternalItem(ExternalItem externalItem, CancellationToken cancellationToken) + { + // TODO: + await Task.Yield(); + } + private async ValueTask DownloadOrOpenPage(NexusModsFileMetadata.ReadOnly fileMetadata, CancellationToken cancellationToken) { if (_loginManager.IsPremium) diff --git a/src/NexusMods.App.UI/Pages/CollectionDownload/ExternalDownloadItemModel.cs b/src/NexusMods.App.UI/Pages/CollectionDownload/ExternalDownloadItemModel.cs new file mode 100644 index 0000000000..d472bc599f --- /dev/null +++ b/src/NexusMods.App.UI/Pages/CollectionDownload/ExternalDownloadItemModel.cs @@ -0,0 +1,80 @@ +using NexusMods.Abstractions.Jobs; +using NexusMods.Abstractions.NexusModsLibrary.Models; +using NexusMods.App.UI.Controls; +using NexusMods.App.UI.Extensions; +using NexusMods.App.UI.Pages.LibraryPage; +using NexusMods.MnemonicDB.Abstractions; +using NexusMods.Paths; +using R3; + +namespace NexusMods.App.UI.Pages.CollectionDownload; + +public class ExternalDownloadItemModel : TreeDataGridItemModel, + ILibraryItemWithName, + ILibraryItemWithSize, + ILibraryItemWithDownloadAction +{ + public ExternalDownloadItemModel(CollectionDownloadExternal.ReadOnly externalDownload) + { + DownloadableItem = new DownloadableItem(new ExternalItem(externalDownload.Uri, externalDownload.Size, externalDownload.Md5)); + FormattedSize = ItemSize.ToFormattedProperty(); + DownloadItemCommand = ILibraryItemWithDownloadAction.CreateCommand(this); + + // ReSharper disable once NotDisposedResource + var modelActivationDisposable = this.WhenActivated(static (self, disposables) => + { + self.IsInLibraryObservable.ObserveOnUIThreadDispatcher() + .Subscribe(self, static (inLibrary, self) => + { + self.DownloadState.Value = inLibrary ? JobStatus.Completed : JobStatus.None; + self.DownloadButtonText.Value = ILibraryItemWithDownloadAction.GetButtonText(status: self.DownloadState.Value); + }).AddTo(disposables); + }); + + _modelDisposable = Disposable.Combine( + modelActivationDisposable, + Name, + ItemSize, + FormattedSize, + DownloadItemCommand, + DownloadState, + DownloadButtonText + ); + } + + public required Observable IsInLibraryObservable { get; init; } + // public required Observable DownloadJobObservable { get; init; } + + public BindableReactiveProperty Name { get; } = new(value: "-"); + + public ReactiveProperty ItemSize { get; } = new(); + public BindableReactiveProperty FormattedSize { get; } + + public DownloadableItem DownloadableItem { get; } + + public ReactiveCommand DownloadItemCommand { get; } + + public BindableReactiveProperty DownloadState { get; } = new(); + + public BindableReactiveProperty DownloadButtonText { get; } = new(value: ILibraryItemWithDownloadAction.GetButtonText(status: JobStatus.None)); + + private bool _isDisposed; + private readonly IDisposable _modelDisposable; + + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _modelDisposable.Dispose(); + } + + _isDisposed = true; + } + + base.Dispose(disposing); + } + + public override string ToString() => $"External Download: {Name.Value}"; +} diff --git a/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithAction.cs b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithAction.cs index 336b6fdd63..c987f4ed38 100644 --- a/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithAction.cs +++ b/src/NexusMods.App.UI/Pages/LibraryPage/ILibraryItemWithAction.cs @@ -1,7 +1,9 @@ using System.Diagnostics.CodeAnalysis; using NexusMods.Abstractions.Jobs; +using NexusMods.Abstractions.MnemonicDB.Attributes; using NexusMods.Abstractions.NexusModsLibrary; using NexusMods.App.UI.Controls; +using NexusMods.Paths; using OneOf; using R3; @@ -66,9 +68,11 @@ public static string GetButtonText(int numInstalled, int numTotal, bool isExpand } } -public class DownloadableItem : OneOfBase +public record struct ExternalItem(Uri Uri, Size Size, Md5HashValue Md5); + +public class DownloadableItem : OneOfBase { - public DownloadableItem(OneOf input) : base(input) { } + public DownloadableItem(OneOf input) : base(input) { } } public interface ILibraryItemWithDownloadAction : ILibraryItemWithAction diff --git a/src/NexusMods.App.UI/Pages/NexusModsDataProvider.cs b/src/NexusMods.App.UI/Pages/NexusModsDataProvider.cs index 7030fe891c..b690053b12 100644 --- a/src/NexusMods.App.UI/Pages/NexusModsDataProvider.cs +++ b/src/NexusMods.App.UI/Pages/NexusModsDataProvider.cs @@ -5,6 +5,7 @@ using DynamicData.Kernel; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; +using NexusMods.Abstractions.Collections; using NexusMods.Abstractions.Jobs; using NexusMods.Abstractions.Library.Models; using NexusMods.Abstractions.Loadouts; @@ -12,6 +13,7 @@ using NexusMods.Abstractions.NexusModsLibrary; using NexusMods.Abstractions.NexusModsLibrary.Models; using NexusMods.App.UI.Extensions; +using NexusMods.App.UI.Pages.CollectionDownload; using NexusMods.App.UI.Pages.LibraryPage; using NexusMods.App.UI.Pages.LoadoutPage; using NexusMods.Extensions.BCL; @@ -43,7 +45,7 @@ public IObservable> ObserveCollectionIte .ObserveDatoms(CollectionDownloadEntity.CollectionRevision, revisionMetadata) .AsEntityIds() .Transform(datom => CollectionDownloadEntity.Load(_connection.Db, datom.E)) - .Filter(static downloadEntity => downloadEntity.IsCollectionDownloadNexusMods())// TODO: || downloadEntity.IsCollectionDownloadExternal()) + .Filter(static downloadEntity => downloadEntity.IsCollectionDownloadNexusMods() || downloadEntity.IsCollectionDownloadExternal()) .Transform(ILibraryItemModel (downloadEntity) => { if (downloadEntity.TryGetAsCollectionDownloadNexusMods(out var nexusModsDownload)) @@ -106,14 +108,34 @@ private ILibraryItemModel ToLibraryItemModel(CollectionDownloadNexusMods.ReadOnl model.Name.Value = nexusModsDownload.FileMetadata.Name; model.Version.Value = nexusModsDownload.FileMetadata.Version; - - if (NexusModsFileMetadata.Size.TryGet(nexusModsDownload.FileMetadata, out var size)) model.ItemSize.Value = size; + model.ItemSize.Value = nexusModsDownload.FileMetadata.Size; return model; } private ILibraryItemModel ToLibraryItemModel(CollectionDownloadExternal.ReadOnly externalDownload) { - throw new NotImplementedException(); + var isInLibraryObservable = _connection + .ObserveDatoms(DirectDownloadLibraryFile.Md5) + .Transform(datom => DirectDownloadLibraryFile.Md5.ReadValue(datom.ValueSpan, datom.Prefix.ValueTag, _connection.AttributeResolver)) + .Filter(hash => hash == externalDownload.Md5) + .IsNotEmpty() + .ToObservable() + .Prepend((_connection, externalDownload.Md5), static state => + { + var (connection, hash) = state; + var libraryItems = DirectDownloadLibraryFile.FindByMd5(connection.Db, hash); + return libraryItems.Count > 0; + }); + + var model = new ExternalDownloadItemModel(externalDownload) + { + IsInLibraryObservable = isInLibraryObservable, + // DownloadJobObservable = , + }; + + model.Name.Value = externalDownload.AsCollectionDownload().Name; + model.ItemSize.Value = externalDownload.Size; + return model; } public IObservable> ObserveFlatLibraryItems(LibraryFilter libraryFilter)