Multi-Platform Package Manager for Stable Diffusion
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

378 lines
11 KiB

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<PackageExtension, string> availableExtensionsSource =
new(ext => ext.Author + ext.Title + ext.Reference);
public IObservableCollection<SelectableItem<PackageExtension>> SelectedAvailableItems { get; } =
new ObservableCollectionExtended<SelectableItem<PackageExtension>>();
public SearchCollection<
SelectableItem<PackageExtension>,
string,
string
> AvailableItemsSearchCollection { get; }
private SourceCache<InstalledPackageExtension, string> installedExtensionsSource =
new(
ext =>
ext.Paths.FirstOrDefault()?.ToString() ?? ext.GitRepositoryUrl ?? ext.GetHashCode().ToString()
);
public IObservableCollection<SelectableItem<InstalledPackageExtension>> SelectedInstalledItems { get; } =
new ObservableCollectionExtended<SelectableItem<InstalledPackageExtension>>();
public SearchCollection<
SelectableItem<InstalledPackageExtension>,
string,
string
> InstalledItemsSearchCollection { get; }
public IObservableCollection<InstalledPackageExtension> InstalledExtensions { get; } =
new ObservableCollectionExtended<InstalledPackageExtension>();
public PackageExtensionBrowserViewModel(INotificationService notificationService)
{
this.notificationService = notificationService;
var availableItemsChangeSet = availableExtensionsSource
.Connect()
.Transform(ext => new SelectableItem<PackageExtension>(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<InstalledPackageExtension>(ext))
.ObserveOn(SynchronizationContext.Current!)
.Publish();
installedItemsChangeSet
.AutoRefresh(item => item.IsSelected)
.Filter(item => item.IsSelected)
.Bind(SelectedInstalledItems)
.Subscribe();
cleanUp = new CompositeDisposable(
AvailableItemsSearchCollection = new SearchCollection<
SelectableItem<PackageExtension>,
string,
string
>(
availableItemsChangeSet,
query =>
string.IsNullOrWhiteSpace(query)
? _ => true
: x => x.Item.Title.Contains(query, StringComparison.OrdinalIgnoreCase)
),
availableItemsChangeSet.Connect(),
InstalledItemsSearchCollection = new SearchCollection<
SelectableItem<InstalledPackageExtension>,
string,
string
>(
installedItemsChangeSet,
query =>
string.IsNullOrWhiteSpace(query)
? _ => true
: x => x.Item.Title.Contains(query, StringComparison.OrdinalIgnoreCase)
),
installedItemsChangeSet.Connect()
);
}
public void AddExtensions(
IEnumerable<PackageExtension> packageExtensions,
IEnumerable<InstalledPackageExtension> 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<IPackageStep>()
.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<IPackageStep>()
.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<IPackageStep>()
.ToArray();
var runner = new PackageModificationRunner { ShowDialogOnStart = true };
EventManager.Instance.OnPackageInstallProgressAdded(runner);
await runner.ExecuteSteps(steps);
ClearSelection();
RefreshBackground();
}
/// <inheritdoc />
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<PackageExtension>,
IEnumerable<InstalledPackageExtension>
) SynchronizeExtensions(
IEnumerable<PackageExtension> extensions,
IEnumerable<InstalledPackageExtension> 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);
}
/// <inheritdoc />
public void Dispose()
{
availableExtensionsSource.Dispose();
installedExtensionsSource.Dispose();
cleanUp.Dispose();
GC.SuppressFinalize(this);
}
}