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.
308 lines
9.0 KiB
308 lines
9.0 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.Linq; |
|
using System.Reactive.Linq; |
|
using System.Threading; |
|
using System.Threading.Tasks; |
|
using AsyncAwaitBestPractices; |
|
using Avalonia.Controls; |
|
using Avalonia.Controls.Primitives; |
|
using Avalonia.Threading; |
|
using CommunityToolkit.Mvvm.ComponentModel; |
|
using CommunityToolkit.Mvvm.Input; |
|
using DynamicData; |
|
using DynamicData.Binding; |
|
using FluentAvalonia.UI.Controls; |
|
using StabilityMatrix.Avalonia.Controls; |
|
using StabilityMatrix.Avalonia.Languages; |
|
using StabilityMatrix.Avalonia.ViewModels.Base; |
|
using StabilityMatrix.Avalonia.Views.Dialogs; |
|
using StabilityMatrix.Core.Attributes; |
|
using StabilityMatrix.Core.Extensions; |
|
using StabilityMatrix.Core.Helper; |
|
using StabilityMatrix.Core.Models; |
|
using StabilityMatrix.Core.Models.FileInterfaces; |
|
using StabilityMatrix.Core.Models.PackageModification; |
|
using StabilityMatrix.Core.Processes; |
|
using StabilityMatrix.Core.Python; |
|
|
|
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; |
|
|
|
[View(typeof(PythonPackagesDialog))] |
|
[ManagedService] |
|
[Transient] |
|
public partial class PythonPackagesViewModel : ContentDialogViewModelBase |
|
{ |
|
public DirectoryPath? VenvPath { get; set; } |
|
|
|
[ObservableProperty] |
|
private bool isLoading; |
|
|
|
private readonly SourceCache<PipPackageInfo, string> packageSource = new(p => p.Name); |
|
|
|
public IObservableCollection<PythonPackagesItemViewModel> Packages { get; } = |
|
new ObservableCollectionExtended<PythonPackagesItemViewModel>(); |
|
|
|
[ObservableProperty] |
|
private PythonPackagesItemViewModel? selectedPackage; |
|
|
|
public PythonPackagesViewModel() |
|
{ |
|
packageSource |
|
.Connect() |
|
.DeferUntilLoaded() |
|
.Transform(p => new PythonPackagesItemViewModel { Package = p }) |
|
.SortBy(vm => vm.Package.Name) |
|
.Bind(Packages) |
|
.Subscribe(); |
|
} |
|
|
|
private async Task Refresh() |
|
{ |
|
if (VenvPath is null) |
|
return; |
|
|
|
IsLoading = true; |
|
|
|
try |
|
{ |
|
if (Design.IsDesignMode) |
|
{ |
|
await Task.Delay(250); |
|
} |
|
else |
|
{ |
|
await using var venvRunner = new PyVenvRunner(VenvPath); |
|
|
|
var packages = await venvRunner.PipList(); |
|
packageSource.EditDiff(packages); |
|
} |
|
} |
|
finally |
|
{ |
|
IsLoading = false; |
|
} |
|
} |
|
|
|
[RelayCommand] |
|
private async Task RefreshBackground() |
|
{ |
|
if (VenvPath is null) |
|
return; |
|
|
|
await using var venvRunner = new PyVenvRunner(VenvPath); |
|
|
|
var packages = await venvRunner.PipList(); |
|
|
|
Dispatcher.UIThread.Post(() => |
|
{ |
|
// Backup selected package |
|
var currentPackageName = SelectedPackage?.Package.Name; |
|
|
|
packageSource.EditDiff(packages); |
|
|
|
// Restore selected package |
|
SelectedPackage = Packages.FirstOrDefault(p => p.Package.Name == currentPackageName); |
|
}); |
|
} |
|
|
|
/// <summary> |
|
/// Load the selected package's show info if not already loaded |
|
/// </summary> |
|
partial void OnSelectedPackageChanged(PythonPackagesItemViewModel? value) |
|
{ |
|
if (value is null) |
|
{ |
|
return; |
|
} |
|
|
|
if (value.PipShowResult is null) |
|
{ |
|
value.LoadExtraInfo(VenvPath!).SafeFireAndForget(); |
|
} |
|
} |
|
|
|
/// <inheritdoc /> |
|
public override Task OnLoadedAsync() |
|
{ |
|
return Refresh(); |
|
} |
|
|
|
public void AddPackages(params PipPackageInfo[] packages) |
|
{ |
|
packageSource.AddOrUpdate(packages); |
|
} |
|
|
|
[RelayCommand] |
|
private Task ModifySelectedPackage(PythonPackagesItemViewModel? item) |
|
{ |
|
return item?.SelectedVersion != null |
|
? UpgradePackageVersion( |
|
item.Package.Name, |
|
item.SelectedVersion, |
|
PythonPackagesItemViewModel.GetKnownIndexUrl(item.Package.Name, item.SelectedVersion), |
|
isDowngrade: item.CanDowngrade |
|
) |
|
: Task.CompletedTask; |
|
} |
|
|
|
private async Task UpgradePackageVersion( |
|
string packageName, |
|
string version, |
|
string? extraIndexUrl = null, |
|
bool isDowngrade = false |
|
) |
|
{ |
|
if (VenvPath is null || SelectedPackage?.Package is not { } package) |
|
return; |
|
|
|
// Confirmation dialog |
|
var dialog = DialogHelper.CreateMarkdownDialog( |
|
isDowngrade |
|
? $"Downgrade **{package.Name}** to **{version}**?" |
|
: $"Upgrade **{package.Name}** to **{version}**?", |
|
Resources.Label_ConfirmQuestion |
|
); |
|
|
|
dialog.PrimaryButtonText = isDowngrade ? Resources.Action_Downgrade : Resources.Action_Upgrade; |
|
dialog.IsPrimaryButtonEnabled = true; |
|
dialog.DefaultButton = ContentDialogButton.Primary; |
|
dialog.CloseButtonText = Resources.Action_Cancel; |
|
|
|
if (await dialog.ShowAsync() is not ContentDialogResult.Primary) |
|
{ |
|
return; |
|
} |
|
|
|
var args = new ProcessArgsBuilder("install", $"{packageName}=={version}"); |
|
|
|
if (extraIndexUrl != null) |
|
{ |
|
args = args.AddArg(("--extra-index-url", extraIndexUrl)); |
|
} |
|
|
|
var steps = new List<IPackageStep> |
|
{ |
|
new PipStep |
|
{ |
|
VenvDirectory = VenvPath, |
|
WorkingDirectory = VenvPath.Parent, |
|
Args = args |
|
} |
|
}; |
|
|
|
var runner = new PackageModificationRunner |
|
{ |
|
ShowDialogOnStart = true, |
|
ModificationCompleteMessage = isDowngrade |
|
? $"Downgraded Python Package '{packageName}' to {version}" |
|
: $"Upgraded Python Package '{packageName}' to {version}" |
|
}; |
|
EventManager.Instance.OnPackageInstallProgressAdded(runner); |
|
await runner.ExecuteSteps(steps); |
|
|
|
// Refresh |
|
RefreshBackground().SafeFireAndForget(); |
|
} |
|
|
|
[RelayCommand] |
|
private async Task InstallPackage() |
|
{ |
|
if (VenvPath is null) |
|
return; |
|
|
|
// Dialog |
|
var fields = new TextBoxField[] |
|
{ |
|
new() { Label = "Package Name", InnerLeftText = "pip install" } |
|
}; |
|
|
|
var dialog = DialogHelper.CreateTextEntryDialog("Install Package", "", fields); |
|
var result = await dialog.ShowAsync(); |
|
|
|
if (result != ContentDialogResult.Primary || fields[0].Text is not { } packageArgs) |
|
{ |
|
return; |
|
} |
|
|
|
var steps = new List<IPackageStep> |
|
{ |
|
new PipStep |
|
{ |
|
VenvDirectory = VenvPath, |
|
WorkingDirectory = VenvPath.Parent, |
|
Args = new ProcessArgs(packageArgs).Prepend("install") |
|
} |
|
}; |
|
|
|
var runner = new PackageModificationRunner |
|
{ |
|
ShowDialogOnStart = true, |
|
ModificationCompleteMessage = $"Installed Python Package '{packageArgs}'" |
|
}; |
|
EventManager.Instance.OnPackageInstallProgressAdded(runner); |
|
await runner.ExecuteSteps(steps); |
|
|
|
// Refresh |
|
RefreshBackground().SafeFireAndForget(); |
|
} |
|
|
|
[RelayCommand] |
|
private async Task UninstallSelectedPackage() |
|
{ |
|
if (VenvPath is null || SelectedPackage?.Package is not { } package) |
|
return; |
|
|
|
// Confirmation dialog |
|
var dialog = DialogHelper.CreateMarkdownDialog( |
|
$"This will uninstall the package '{package.Name}'", |
|
Resources.Label_ConfirmQuestion |
|
); |
|
dialog.PrimaryButtonText = Resources.Action_Uninstall; |
|
dialog.IsPrimaryButtonEnabled = true; |
|
dialog.DefaultButton = ContentDialogButton.Primary; |
|
dialog.CloseButtonText = Resources.Action_Cancel; |
|
|
|
if (await dialog.ShowAsync() is not ContentDialogResult.Primary) |
|
{ |
|
return; |
|
} |
|
|
|
var steps = new List<IPackageStep> |
|
{ |
|
new PipStep |
|
{ |
|
VenvDirectory = VenvPath, |
|
WorkingDirectory = VenvPath.Parent, |
|
Args = new[] { "uninstall", "--yes", package.Name } |
|
} |
|
}; |
|
|
|
var runner = new PackageModificationRunner |
|
{ |
|
ShowDialogOnStart = true, |
|
ModificationCompleteMessage = $"Uninstalled Python Package '{package.Name}'" |
|
}; |
|
EventManager.Instance.OnPackageInstallProgressAdded(runner); |
|
await runner.ExecuteSteps(steps); |
|
|
|
// Refresh |
|
RefreshBackground().SafeFireAndForget(); |
|
} |
|
|
|
public BetterContentDialog GetDialog() |
|
{ |
|
return new BetterContentDialog |
|
{ |
|
CloseOnClickOutside = true, |
|
MinDialogWidth = 800, |
|
MaxDialogWidth = 1500, |
|
FullSizeDesired = true, |
|
ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled, |
|
Title = Resources.Label_PythonPackages, |
|
Content = new PythonPackagesDialog { DataContext = this }, |
|
CloseButtonText = Resources.Action_Close, |
|
DefaultButton = ContentDialogButton.Close |
|
}; |
|
} |
|
}
|
|
|