using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.Contracts; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Threading; 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; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages.Extensions; namespace StabilityMatrix.Avalonia.ViewModels.PackageManager; [View(typeof(PackageExtensionBrowserView))] [Transient] [ManagedService] public partial class PackageExtensionBrowserViewModel : ViewModelBase, IDisposable { private readonly INotificationService notificationService; private readonly IDisposable cleanUp; public PackagePair? PackagePair { get; set; } [ObservableProperty] private bool isLoading; private SourceCache availableExtensionsSource = new(ext => ext.Author + ext.Title + ext.Reference); 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(INotificationService notificationService) { this.notificationService = notificationService; var availableItemsChangeSet = availableExtensionsSource .Connect() .Transform(ext => new SelectableItem(ext)) .ObserveOn(SynchronizationContext.Current!) .Publish(); availableItemsChangeSet .AutoRefresh(item => item.IsSelected) .Filter(item => item.IsSelected) .Bind(SelectedAvailableItems) .Subscribe(); var installedItemsChangeSet = installedExtensionsSource .Connect() .Transform(ext => new SelectableItem(ext)) .ObserveOn(SynchronizationContext.Current!) .Publish(); installedItemsChangeSet .AutoRefresh(item => item.IsSelected) .Filter(item => item.IsSelected) .Bind(SelectedInstalledItems) .Subscribe(); 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( IEnumerable packageExtensions, IEnumerable installedExtensions ) { availableExtensionsSource.AddOrUpdate(packageExtensions); installedExtensionsSource.AddOrUpdate(installedExtensions); } [RelayCommand] public async Task InstallSelectedExtensions() { var extensions = SelectedAvailableItems.Select(x => x.Item).ToArray(); if (extensions.Length == 0) return; var steps = extensions .Select( ext => new InstallExtensionStep( PackagePair!.BasePackage.ExtensionManager!, PackagePair.InstalledPackage, ext ) ) .Cast() .ToArray(); var runner = new PackageModificationRunner { ShowDialogOnStart = true }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); ClearSelection(); RefreshBackground(); } [RelayCommand] public async Task UpdateSelectedExtensions() { var extensions = SelectedInstalledItems.Select(x => x.Item).ToArray(); if (extensions.Length == 0) return; var steps = extensions .Select( ext => new UpdateExtensionStep( PackagePair!.BasePackage.ExtensionManager!, PackagePair.InstalledPackage, ext ) ) .Cast() .ToArray(); var runner = new PackageModificationRunner { ShowDialogOnStart = true }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); ClearSelection(); RefreshBackground(); } [RelayCommand] public async Task UninstallSelectedExtensions() { var extensions = SelectedInstalledItems.Select(x => x.Item).ToArray(); if (extensions.Length == 0) return; var steps = extensions .Select( ext => new UninstallExtensionStep( PackagePair!.BasePackage.ExtensionManager!, PackagePair.InstalledPackage, ext ) ) .Cast() .ToArray(); var runner = new PackageModificationRunner { ShowDialogOnStart = true }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); ClearSelection(); RefreshBackground(); } /// public override async Task OnLoadedAsync() { await base.OnLoadedAsync(); await Refresh(); } [RelayCommand] public async Task Refresh() { if (PackagePair is null) return; IsLoading = true; try { if (Design.IsDesignMode) { var (availableExtensions, installedExtensions) = SynchronizeExtensions( availableExtensionsSource.Items, installedExtensionsSource.Items ); availableExtensionsSource.EditDiff(availableExtensions); installedExtensionsSource.EditDiff(installedExtensions); await Task.Delay(250); } else { await RefreshCore(); } } finally { IsLoading = false; } } public void RefreshBackground() { RefreshCore() .SafeFireAndForget(ex => { notificationService.ShowPersistent("Failed to refresh extensions", ex.ToString()); }); } private async Task RefreshCore() { using var _ = CodeTimer.StartDebug(); if (PackagePair?.BasePackage.ExtensionManager is not { } extensionManager) { throw new NotSupportedException( $"The package {PackagePair?.BasePackage} does not support extensions." ); } var availableExtensions = await extensionManager.GetManifestExtensionsAsync( extensionManager.GetManifests(PackagePair.InstalledPackage) ); var installedExtensions = await extensionManager.GetInstalledExtensionsAsync( PackagePair.InstalledPackage ); // Synchronize (availableExtensions, installedExtensions) = SynchronizeExtensions( availableExtensions, installedExtensions ); await Task.Run(() => { availableExtensionsSource.Edit(updater => { updater.Load(availableExtensions); }); installedExtensionsSource.Edit(updater => { updater.Load(installedExtensions); }); }); } public void ClearSelection() { foreach (var item in SelectedAvailableItems.ToImmutableArray()) { 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); } /// public void Dispose() { availableExtensionsSource.Dispose(); installedExtensionsSource.Dispose(); cleanUp.Dispose(); GC.SuppressFinalize(this); } }