diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 3ac1ac7a..510a7046 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -37,6 +37,7 @@ using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Database; +using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages.Extensions; @@ -487,14 +488,24 @@ public static class DesignData DialogFactory.Get(vm => { vm.AddExtensions( - new PackageExtension - { - Author = "123", - Title = "Cool Extension", - Description = "This is an interesting extension", - Reference = new Uri("https://github.com/LykosAI/StabilityMatrix"), - Files = [new Uri("https://github.com/LykosAI/StabilityMatrix")] - } + [ + new PackageExtension + { + Author = "123", + Title = "Cool Extension", + Description = "This is an interesting extension", + Reference = new Uri("https://github.com/LykosAI/StabilityMatrix"), + Files = [new Uri("https://github.com/LykosAI/StabilityMatrix")] + } + ], + [ + new InstalledPackageExtension + { + GitRepositoryUrl = "https://github.com/LykosAI/StabilityMatrix", + Paths = [new DirectoryPath("example-dir")] + }, + new InstalledPackageExtension { Paths = [new DirectoryPath("example-dir-2")] } + ] ); }); diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageExtensionBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageExtensionBrowserViewModel.cs index 46ea684b..84f08ffa 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageExtensionBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageExtensionBrowserViewModel.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; +using System.Diagnostics.Contracts; using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls; @@ -9,6 +11,8 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; +using StabilityMatrix.Avalonia.Collections; +using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.PackageManager; using StabilityMatrix.Core.Attributes; @@ -23,15 +27,12 @@ namespace StabilityMatrix.Avalonia.ViewModels.PackageManager; [View(typeof(PackageExtensionBrowserView))] [Transient] [ManagedService] -public partial class PackageExtensionBrowserViewModel : ViewModelBase +public partial class PackageExtensionBrowserViewModel : ViewModelBase, IDisposable { - public PackagePair? PackagePair { get; set; } - - [ObservableProperty] - private string searchFilter = string.Empty; + private readonly INotificationService notificationService; + private readonly IDisposable cleanUp; - [ObservableProperty] - private int selectedItemsCount; + public PackagePair? PackagePair { get; set; } [ObservableProperty] private bool isLoading; @@ -39,67 +40,94 @@ public partial class PackageExtensionBrowserViewModel : ViewModelBase private SourceCache availableExtensionsSource = new(ext => ext.Author + ext.Title + ext.Reference); - public IObservableCollection> AvailableItems { get; } = - new ObservableCollectionExtended>(); - - public IObservableCollection> AvailableItemsFiltered { get; } = - new ObservableCollectionExtended>(); - public IObservableCollection> SelectedAvailableItems { get; } = new ObservableCollectionExtended>(); + public SearchCollection< + SelectableItem, + string, + string + > AvailableItemsSearchCollection { get; } + private SourceCache installedExtensionsSource = new( ext => ext.Paths.FirstOrDefault()?.ToString() ?? ext.GitRepositoryUrl ?? ext.GetHashCode().ToString() ); + public IObservableCollection> SelectedInstalledItems { get; } = + new ObservableCollectionExtended>(); + + public SearchCollection< + SelectableItem, + string, + string + > InstalledItemsSearchCollection { get; } + public IObservableCollection InstalledExtensions { get; } = new ObservableCollectionExtended(); - public PackageExtensionBrowserViewModel() + public PackageExtensionBrowserViewModel(INotificationService notificationService) { - var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchFilter) - .Select( - change => - string.IsNullOrWhiteSpace(change.Value) - ? _ => true - : new Func, bool>( - ext => ext.Item.Title.Contains(change.Value, StringComparison.OrdinalIgnoreCase) - ) - ) - .AsObservable(); + this.notificationService = notificationService; var availableItemsChangeSet = availableExtensionsSource .Connect() .Transform(ext => new SelectableItem(ext)) .Publish(); - availableItemsChangeSet - .SortBy(x => x.Item.Title) - .Bind(AvailableItems) - .Filter(searchPredicate) - .Bind(AvailableItemsFiltered) - .Subscribe(); - availableItemsChangeSet .AutoRefresh(item => item.IsSelected) .Filter(item => item.IsSelected) - .ForEachChange(OnSelectedItemsUpdate) .Bind(SelectedAvailableItems) .Subscribe(); - availableItemsChangeSet.Connect(); + var installedItemsChangeSet = installedExtensionsSource + .Connect() + .Transform(ext => new SelectableItem(ext)) + .Publish(); + + installedItemsChangeSet + .AutoRefresh(item => item.IsSelected) + .Filter(item => item.IsSelected) + .Bind(SelectedInstalledItems) + .Subscribe(); - SelectedAvailableItems - .WhenPropertyChanged(x => x.Count) - .Select(x => x.Value) - .Subscribe(x => SelectedItemsCount = x); + cleanUp = new CompositeDisposable( + AvailableItemsSearchCollection = new SearchCollection< + SelectableItem, + string, + string + >( + availableItemsChangeSet, + query => + string.IsNullOrWhiteSpace(query) + ? _ => true + : x => x.Item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) + ), + availableItemsChangeSet.Connect(), + InstalledItemsSearchCollection = new SearchCollection< + SelectableItem, + string, + string + >( + installedItemsChangeSet, + query => + string.IsNullOrWhiteSpace(query) + ? _ => true + : x => x.Item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) + ), + installedItemsChangeSet.Connect() + ); } - public void AddExtensions(params PackageExtension[] packageExtensions) + public void AddExtensions( + IEnumerable packageExtensions, + IEnumerable installedExtensions + ) { availableExtensionsSource.AddOrUpdate(packageExtensions); + installedExtensionsSource.AddOrUpdate(installedExtensions); } [RelayCommand] @@ -156,23 +184,34 @@ public partial class PackageExtensionBrowserViewModel : ViewModelBase { if (Design.IsDesignMode) { + var (availableExtensions, installedExtensions) = SynchronizeExtensions( + availableExtensionsSource.Items, + installedExtensionsSource.Items + ); + + availableExtensionsSource.EditDiff(availableExtensions); + installedExtensionsSource.EditDiff(installedExtensions); + await Task.Delay(250); } else { - // Refresh installed + var availableExtensions = await extensionManager.GetManifestExtensionsAsync( + extensionManager.GetManifests(PackagePair.InstalledPackage) + ); + var installedExtensions = await extensionManager.GetInstalledExtensionsAsync( PackagePair.InstalledPackage ); - installedExtensionsSource.EditDiff(installedExtensions); - - // Refresh available - var extensions = await extensionManager.GetManifestExtensionsAsync( - extensionManager.GetManifests(PackagePair.InstalledPackage) + // Synchronize + (availableExtensions, installedExtensions) = SynchronizeExtensions( + availableExtensions, + installedExtensions ); - availableExtensionsSource.EditDiff(extensions); + availableExtensionsSource.EditDiff(availableExtensions); + installedExtensionsSource.EditDiff(installedExtensions); } } finally @@ -187,10 +226,58 @@ public partial class PackageExtensionBrowserViewModel : ViewModelBase { item.IsSelected = false; } + + foreach (var item in SelectedInstalledItems.ToImmutableArray()) + { + item.IsSelected = false; + } + } + + [Pure] + private static ( + IEnumerable, + IEnumerable + ) SynchronizeExtensions( + IEnumerable extensions, + IEnumerable installedExtensions + ) + { + using var _ = CodeTimer.StartDebug(); + + var extensionsArr = extensions.ToImmutableArray(); + + // For extensions, map their file paths for lookup + var repoToExtension = extensionsArr + .SelectMany(ext => ext.Files.Select(path => (path, ext))) + .ToLookup(kv => kv.path.ToString().StripEnd(".git")) + .ToDictionary(group => group.Key, x => x.First().ext); + + // For installed extensions, add remote repo if available + var installedExtensionsWithDefinition = installedExtensions.Select( + installedExt => + installedExt.GitRepositoryUrl is null + || !repoToExtension.TryGetValue( + installedExt.GitRepositoryUrl.StripEnd(".git"), + out var mappedExt + ) + ? installedExt + : installedExt with + { + Definition = mappedExt + } + ); + + return (extensionsArr, installedExtensionsWithDefinition); } - private void OnSelectedItemsUpdate(Change, string> change) + /// + public void Dispose() { - Debug.WriteLine($"{change}"); + availableExtensionsSource.Dispose(); + installedExtensionsSource.Dispose(); + + cleanUp.Dispose(); + + GC.SuppressFinalize(this); } } diff --git a/StabilityMatrix.Avalonia/Views/PackageManager/PackageExtensionBrowserView.axaml b/StabilityMatrix.Avalonia/Views/PackageManager/PackageExtensionBrowserView.axaml index a9f70383..2bdf1c52 100644 --- a/StabilityMatrix.Avalonia/Views/PackageManager/PackageExtensionBrowserView.axaml +++ b/StabilityMatrix.Avalonia/Views/PackageManager/PackageExtensionBrowserView.axaml @@ -14,6 +14,7 @@ xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia" xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" + xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" d:DataContext="{x:Static mocks:DesignData.PackageExtensionBrowserViewModel}" x:DataType="packageManager:PackageExtensionBrowserViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" @@ -21,6 +22,7 @@ + @@ -68,17 +70,70 @@ Margin="0,0,16,0" Grid.Column="2"/> - + + + + + + + + + + + + + + + + + + + + + + + + - + + Text="{Binding Query, Mode=TwoWay}"> @@ -133,19 +189,31 @@ + ItemsSource="{Binding FilteredItems}"> + + + + @@ -163,53 +231,108 @@ - + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + +