Browse Source

Merge pull request #193 from ionite34/revamped-packages-tab

New package manager UI
pull/109/head
JT 1 year ago committed by GitHub
parent
commit
166ace4874
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 11
      StabilityMatrix.Avalonia/App.axaml.cs
  3. 73
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  4. 1
      StabilityMatrix.Avalonia/Program.cs
  5. 1
      StabilityMatrix.Avalonia/Services/ServiceManager.cs
  6. 29
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserCardViewModel.cs
  7. 13
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  8. 2
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  9. 4
      StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
  10. 289
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  11. 313
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  12. 2
      StabilityMatrix.Avalonia/ViewModels/ProgressViewModel.cs
  13. 278
      StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml
  14. 1
      StabilityMatrix.Core/Database/LiteDbContext.cs
  15. 2
      StabilityMatrix.Core/Models/InstalledPackage.cs
  16. 5
      StabilityMatrix.Core/Services/SettingsManager.cs

4
CHANGELOG.md

@ -5,6 +5,10 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.2.0
### Changed
- Revamped Package Manager UI
## v2.1.2 ## v2.1.2
### Changed ### Changed

11
StabilityMatrix.Avalonia/App.axaml.cs

@ -38,6 +38,7 @@ using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api;
@ -200,7 +201,6 @@ public sealed class App : Application
services.AddSingleton<PackageManagerViewModel>() services.AddSingleton<PackageManagerViewModel>()
.AddSingleton<SettingsViewModel>() .AddSingleton<SettingsViewModel>()
.AddSingleton<CheckpointBrowserViewModel>() .AddSingleton<CheckpointBrowserViewModel>()
.AddSingleton<CheckpointBrowserCardViewModel>()
.AddSingleton<CheckpointsPageViewModel>() .AddSingleton<CheckpointsPageViewModel>()
.AddSingleton<LaunchPageViewModel>() .AddSingleton<LaunchPageViewModel>()
.AddSingleton<ProgressManagerViewModel>(); .AddSingleton<ProgressManagerViewModel>();
@ -242,8 +242,11 @@ public sealed class App : Application
services.AddSingleton<UpdateViewModel>(); services.AddSingleton<UpdateViewModel>();
// Other transients (usually sub view models) // Other transients (usually sub view models)
services.AddTransient<CheckpointFolder>(); services.AddTransient<CheckpointFolder>()
services.AddTransient<CheckpointFile>(); .AddTransient<CheckpointFile>()
.AddTransient<CheckpointBrowserCardViewModel>();
services.AddTransient<PackageCardViewModel>();
// Global progress // Global progress
services.AddSingleton<ProgressManagerViewModel>(); services.AddSingleton<ProgressManagerViewModel>();
@ -260,8 +263,10 @@ public sealed class App : Application
.Register(provider.GetRequiredService<SelectDataDirectoryViewModel>) .Register(provider.GetRequiredService<SelectDataDirectoryViewModel>)
.Register(provider.GetRequiredService<LaunchOptionsViewModel>) .Register(provider.GetRequiredService<LaunchOptionsViewModel>)
.Register(provider.GetRequiredService<UpdateViewModel>) .Register(provider.GetRequiredService<UpdateViewModel>)
.Register(provider.GetRequiredService<CheckpointBrowserCardViewModel>)
.Register(provider.GetRequiredService<CheckpointFolder>) .Register(provider.GetRequiredService<CheckpointFolder>)
.Register(provider.GetRequiredService<CheckpointFile>) .Register(provider.GetRequiredService<CheckpointFile>)
.Register(provider.GetRequiredService<PackageCardViewModel>)
.Register(provider.GetRequiredService<RefreshBadgeViewModel>) .Register(provider.GetRequiredService<RefreshBadgeViewModel>)
.Register(provider.GetRequiredService<ExceptionViewModel>) .Register(provider.GetRequiredService<ExceptionViewModel>)
.Register(provider.GetRequiredService<EnvVarsViewModel>) .Register(provider.GetRequiredService<EnvVarsViewModel>)

73
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -4,12 +4,14 @@ using System.Collections.Immutable;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
@ -35,7 +37,8 @@ public static class DesignData
// or else Avalonia analyzers won't work. // or else Avalonia analyzers won't work.
public static void Initialize() public static void Initialize()
{ {
if (isInitialized) throw new InvalidOperationException("DesignData is already initialized."); if (isInitialized)
throw new InvalidOperationException("DesignData is already initialized.");
var services = new ServiceCollection(); var services = new ServiceCollection();
@ -50,6 +53,7 @@ public static class DesignData
{ {
Id = activePackageId, Id = activePackageId,
DisplayName = "My Installed Package", DisplayName = "My Installed Package",
DisplayVersion = "v1.0.0",
PackageName = "stable-diffusion-webui", PackageName = "stable-diffusion-webui",
PackageVersion = "v1.0.0", PackageVersion = "v1.0.0",
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
@ -59,8 +63,8 @@ public static class DesignData
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
DisplayName = "Dank Diffusion", DisplayName = "Dank Diffusion",
PackageName = "dank-diffusion", PackageName = "ComfyUI",
PackageVersion = "v2.0.0", DisplayVersion = "main@ab73d4a",
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now LastUpdateCheck = DateTimeOffset.Now
} }
@ -120,13 +124,13 @@ public static class DesignData
Type = LaunchOptionType.String, Type = LaunchOptionType.String,
Description = "The host name for the Web UI", Description = "The host name for the Web UI",
DefaultValue = "localhost", DefaultValue = "localhost",
Options = { "--host" } Options = {"--host"}
}), }),
LaunchOptionCard.FromDefinition(new LaunchOptionDefinition LaunchOptionCard.FromDefinition(new LaunchOptionDefinition
{ {
Name = "API", Name = "API",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
Options = { "--api" } Options = {"--api"}
}) })
}; };
LaunchOptionsViewModel.UpdateFilterCards(); LaunchOptionsViewModel.UpdateFilterCards();
@ -204,24 +208,29 @@ public static class DesignData
CheckpointBrowserViewModel.ModelCards = new CheckpointBrowserViewModel.ModelCards = new
ObservableCollection<CheckpointBrowserCardViewModel> ObservableCollection<CheckpointBrowserCardViewModel>
{ {
new(new CivitModel dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = new CivitModel
{ {
Name = "BB95 Furry Mix", Name = "BB95 Furry Mix",
Description = "A furry mix of BB95", Description = "A furry mix of BB95",
}, downloadService, settingsManager, };
dialogFactory, notificationService) })
}; };
ProgressManagerViewModel.ProgressItems = new ObservableCollection<ProgressItemViewModel> ProgressManagerViewModel.ProgressItems = new ObservableCollection<ProgressItemViewModel>
{ {
new(new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressReport(0.5f, "Downloading..."))), new(new ProgressItem(Guid.NewGuid(), "Test File.exe",
new(new ProgressItem(Guid.NewGuid(), "Test File 2.uwu", new ProgressReport(0.25f, "Extracting..."))) new ProgressReport(0.5f, "Downloading..."))),
new(new ProgressItem(Guid.NewGuid(), "Test File 2.uwu",
new ProgressReport(0.25f, "Extracting...")))
}; };
UpdateViewModel = Services.GetRequiredService<UpdateViewModel>(); UpdateViewModel = Services.GetRequiredService<UpdateViewModel>();
UpdateViewModel.UpdateText = UpdateViewModel.UpdateText =
$"Stability Matrix v2.0.1 is now available! You currently have v2.0.0. Would you like to update now?"; $"Stability Matrix v2.0.1 is now available! You currently have v2.0.0. Would you like to update now?";
UpdateViewModel.ReleaseNotes = "## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature"; UpdateViewModel.ReleaseNotes =
"## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature";
isInitialized = true; isInitialized = true;
} }
@ -232,20 +241,39 @@ public static class DesignData
public static ServiceManager<ViewModelBase> DialogFactory => public static ServiceManager<ViewModelBase> DialogFactory =>
Services.GetRequiredService<ServiceManager<ViewModelBase>>(); Services.GetRequiredService<ServiceManager<ViewModelBase>>();
public static MainWindowViewModel MainWindowViewModel => public static MainWindowViewModel MainWindowViewModel =>
Services.GetRequiredService<MainWindowViewModel>(); Services.GetRequiredService<MainWindowViewModel>();
public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel => public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel =>
Services.GetRequiredService<FirstLaunchSetupViewModel>(); Services.GetRequiredService<FirstLaunchSetupViewModel>();
public static LaunchPageViewModel LaunchPageViewModel => public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>(); Services.GetRequiredService<LaunchPageViewModel>();
public static PackageManagerViewModel PackageManagerViewModel =>
Services.GetRequiredService<PackageManagerViewModel>(); public static PackageManagerViewModel PackageManagerViewModel
{
get
{
var settings = Services.GetRequiredService<ISettingsManager>();
var vm = Services.GetRequiredService<PackageManagerViewModel>();
vm.Packages = new ObservableCollection<PackageCardViewModel>(
settings.Settings.InstalledPackages.Select(p =>
DialogFactory.Get<PackageCardViewModel>(viewModel => viewModel.Package = p)));
vm.Packages.First().IsUpdateAvailable = true;
return vm;
}
}
public static CheckpointsPageViewModel CheckpointsPageViewModel => public static CheckpointsPageViewModel CheckpointsPageViewModel =>
Services.GetRequiredService<CheckpointsPageViewModel>(); Services.GetRequiredService<CheckpointsPageViewModel>();
public static SettingsViewModel SettingsViewModel => public static SettingsViewModel SettingsViewModel =>
Services.GetRequiredService<SettingsViewModel>(); Services.GetRequiredService<SettingsViewModel>();
public static CheckpointBrowserViewModel CheckpointBrowserViewModel => public static CheckpointBrowserViewModel CheckpointBrowserViewModel =>
Services.GetRequiredService<CheckpointBrowserViewModel>(); Services.GetRequiredService<CheckpointBrowserViewModel>();
public static SelectModelVersionViewModel SelectModelVersionViewModel => public static SelectModelVersionViewModel SelectModelVersionViewModel =>
DialogFactory.Get<SelectModelVersionViewModel>(vm => DialogFactory.Get<SelectModelVersionViewModel>(vm =>
{ {
@ -291,15 +319,19 @@ public static class DesignData
new ModelVersionViewModel(new HashSet<string> {"ABCD"}, sampleCivitVersions[0]); new ModelVersionViewModel(new HashSet<string> {"ABCD"}, sampleCivitVersions[0]);
// Sample data for dialogs // Sample data for dialogs
vm.Versions = new List<ModelVersionViewModel>{sampleViewModel}; vm.Versions = new List<ModelVersionViewModel> {sampleViewModel};
vm.SelectedVersionViewModel = sampleViewModel; vm.SelectedVersionViewModel = sampleViewModel;
}); });
public static OneClickInstallViewModel OneClickInstallViewModel => public static OneClickInstallViewModel OneClickInstallViewModel =>
Services.GetRequiredService<OneClickInstallViewModel>(); Services.GetRequiredService<OneClickInstallViewModel>();
public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel => public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel =>
Services.GetRequiredService<SelectDataDirectoryViewModel>(); Services.GetRequiredService<SelectDataDirectoryViewModel>();
public static ProgressManagerViewModel ProgressManagerViewModel => public static ProgressManagerViewModel ProgressManagerViewModel =>
Services.GetRequiredService<ProgressManagerViewModel>(); Services.GetRequiredService<ProgressManagerViewModel>();
public static ExceptionViewModel ExceptionViewModel => public static ExceptionViewModel ExceptionViewModel =>
DialogFactory.Get<ExceptionViewModel>(viewModel => DialogFactory.Get<ExceptionViewModel>(viewModel =>
{ {
@ -321,13 +353,14 @@ public static class DesignData
} }
}); });
public static EnvVarsViewModel EnvVarsViewModel => DialogFactory.Get<EnvVarsViewModel>(viewModel => public static EnvVarsViewModel EnvVarsViewModel => DialogFactory.Get<EnvVarsViewModel>(
{ viewModel =>
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>
{ {
new("UWU", "TRUE"), viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>
}; {
}); new("UWU", "TRUE"),
};
});
public static RefreshBadgeViewModel RefreshBadgeViewModel => new() public static RefreshBadgeViewModel RefreshBadgeViewModel => new()
{ {

1
StabilityMatrix.Avalonia/Program.cs

@ -12,7 +12,6 @@ using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading; using Avalonia.Threading;
using Microsoft.Extensions.DependencyInjection;
using NLog; using NLog;
using Polly.Contrib.WaitAndRetry; using Polly.Contrib.WaitAndRetry;
using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia;

1
StabilityMatrix.Avalonia/Services/ServiceManager.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace StabilityMatrix.Avalonia.Services; namespace StabilityMatrix.Avalonia.Services;

29
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserCardViewModel.cs

@ -38,33 +38,38 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory; private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
private readonly Action<CheckpointBrowserCardViewModel>? onDownloadStart;
public CivitModel CivitModel { get; init; } public Action<CheckpointBrowserCardViewModel>? OnDownloadStart { get; set; }
public CivitModel CivitModel
{
get => civitModel;
set
{
civitModel = value;
UpdateImage();
CheckIfInstalled();
}
}
public override bool IsTextVisible => Value > 0; public override bool IsTextVisible => Value > 0;
[ObservableProperty] private Uri? cardImage; [ObservableProperty] private Uri? cardImage;
[ObservableProperty] private bool isImporting; [ObservableProperty] private bool isImporting;
[ObservableProperty] private string updateCardText = string.Empty; [ObservableProperty] private string updateCardText = string.Empty;
[ObservableProperty] private bool showUpdateCard; [ObservableProperty] private bool showUpdateCard;
private CivitModel civitModel;
public CheckpointBrowserCardViewModel( public CheckpointBrowserCardViewModel(
CivitModel civitModel,
IDownloadService downloadService, IDownloadService downloadService,
ISettingsManager settingsManager, ISettingsManager settingsManager,
ServiceManager<ViewModelBase> dialogFactory, ServiceManager<ViewModelBase> dialogFactory,
INotificationService notificationService, INotificationService notificationService)
Action<CheckpointBrowserCardViewModel>? onDownloadStart = null)
{ {
this.downloadService = downloadService; this.downloadService = downloadService;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory; this.dialogFactory = dialogFactory;
this.notificationService = notificationService; this.notificationService = notificationService;
this.onDownloadStart = onDownloadStart;
CivitModel = civitModel;
UpdateImage();
CheckIfInstalled();
// Update image when nsfw setting changes // Update image when nsfw setting changes
settingsManager.RegisterPropertyChangedHandler( settingsManager.RegisterPropertyChangedHandler(
@ -197,7 +202,7 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
IsImporting = true; IsImporting = true;
Text = "Downloading..."; Text = "Downloading...";
onDownloadStart?.Invoke(this); OnDownloadStart?.Invoke(this);
// Holds files to be deleted on errors // Holds files to be deleted on errors
var filesForCleanup = new HashSet<FilePath>(); var filesForCleanup = new HashSet<FilePath>();

13
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -228,13 +228,18 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
return cachedViewModel; return cachedViewModel;
} }
var newCard = new CheckpointBrowserCardViewModel(model, var newCard = dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
downloadService, settingsManager, dialogFactory, notificationService, {
viewModel => vm.CivitModel = model;
vm.OnDownloadStart = viewModel =>
{ {
if (cache.Get(viewModel.CivitModel.Id) != null) return; if (cache.Get(viewModel.CivitModel.Id) != null) return;
cache.Add(viewModel.CivitModel.Id, viewModel); cache.Add(viewModel.CivitModel.Id, viewModel);
}); };
return vm;
});
return newCard; return newCard;
}).ToList(); }).ToList();

2
StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Python.Runtime;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
@ -14,7 +13,6 @@ using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater; using StabilityMatrix.Core.Updater;
using Dispatcher = Avalonia.Threading.Dispatcher;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;

4
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

@ -136,9 +136,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
return; return;
} }
OnLoaded(); SelectedPackage = InstalledPackages.FirstOrDefault(x => x.Id == e);
if (SelectedPackage is null) return;
LaunchAsync().SafeFireAndForget(); LaunchAsync().SafeFireAndForget();
} }

289
StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs

@ -0,0 +1,289 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
using NLog;
using Polly;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.PackageManager;
public partial class PackageCardViewModel : ProgressViewModel
{
private readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager;
private readonly Logger logger = LogManager.GetCurrentClassLogger();
[ObservableProperty] private InstalledPackage? package;
[ObservableProperty] private Uri cardImage;
[ObservableProperty] private bool isUpdateAvailable;
[ObservableProperty] private string installedVersion;
public PackageCardViewModel(IPackageFactory packageFactory,
INotificationService notificationService, ISettingsManager settingsManager)
{
this.packageFactory = packageFactory;
this.notificationService = notificationService;
this.settingsManager = settingsManager;
}
partial void OnPackageChanged(InstalledPackage? value)
{
if (string.IsNullOrWhiteSpace(value?.PackageName))
return;
var basePackage = packageFactory[value.PackageName];
CardImage = basePackage?.PreviewImageUri ?? Assets.NoImage;
InstalledVersion = value.DisplayVersion ?? "Unknown";
}
public override async Task OnLoadedAsync()
{
IsUpdateAvailable = await HasUpdate();
}
public void Launch()
{
if (Package == null)
return;
settingsManager.Transaction(s => s.ActiveInstalledPackageId = Package.Id);
EventManager.Instance.RequestPageChange(typeof(LaunchPageViewModel));
EventManager.Instance.OnPackageLaunchRequested(Package.Id);
}
public async Task Uninstall()
{
if (Package?.LibraryPath == null)
{
return;
}
var dialog = new ContentDialog
{
Title = "Are you sure?",
Content = "This will delete all folders in the package directory, including any generated images in that directory as well as any files you may have added.",
PrimaryButtonText = "Yes, delete it",
CloseButtonText = "No, keep it",
DefaultButton = ContentDialogButton.Primary
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
Text = "Uninstalling...";
IsIndeterminate = true;
Value = -1;
var deleteTask = DeleteDirectoryAsync(Path.Combine(settingsManager.LibraryDir,
Package.LibraryPath));
var taskResult = await notificationService.TryAsync(deleteTask,
"Some files could not be deleted. Please close any open files in the package directory and try again.");
if (taskResult.IsSuccessful)
{
notificationService.Show(new Notification("Success",
$"Package {Package.DisplayName} uninstalled",
NotificationType.Success));
settingsManager.Transaction(settings =>
{
settings.RemoveInstalledPackageAndUpdateActive(Package);
});
EventManager.Instance.OnInstalledPackagesChanged();
}
}
}
public async Task Update()
{
if (Package == null) return;
var basePackage = packageFactory[Package.PackageName!];
if (basePackage == null)
{
logger.Error("Could not find package {SelectedPackagePackageName}",
Package.PackageName);
return;
}
Text = $"Updating {Package.DisplayName}";
basePackage.InstallLocation = Package.FullPath!;
var progress = new Progress<ProgressReport>(progress =>
{
var percent = Convert.ToInt32(progress.Percentage);
Value = percent;
IsIndeterminate = progress.IsIndeterminate;
Text = $"Updating {Package.DisplayName}";
EventManager.Instance.OnGlobalProgressChanged(percent);
});
var updateResult = await basePackage.Update(Package, progress);
if (string.IsNullOrWhiteSpace(updateResult))
{
var errorMsg =
$"There was an error updating {Package.DisplayName}. Please try again later.";
if (Package.PackageName == "automatic")
{
errorMsg = errorMsg.Replace("Please try again later.",
"Please stash any changes before updating, or manually update the package.");
}
// there was an error
notificationService.Show(new Notification("Error updating package",
errorMsg, NotificationType.Error));
}
settingsManager.UpdatePackageVersionNumber(Package.Id, updateResult);
notificationService.Show("Update complete",
$"{Package.DisplayName} has been updated to the latest version.",
NotificationType.Success);
Dispatcher.UIThread.Invoke(() =>
{
using (settingsManager.BeginTransaction())
{
Package.UpdateAvailable = false;
}
IsUpdateAvailable = false;
IsIndeterminate = false;
InstalledVersion = settingsManager.Settings.InstalledPackages
.First(x => x.Id == Package.Id).DisplayVersion ?? "Unknown";
Value = 0;
});
}
public async Task OpenFolder()
{
if (string.IsNullOrWhiteSpace(Package?.FullPath))
return;
await ProcessRunner.OpenFolderBrowser(Package.FullPath);
}
private async Task<bool> HasUpdate()
{
if (Package == null)
return false;
var basePackage = packageFactory[Package.PackageName!];
if (basePackage == null)
return false;
var canCheckUpdate = Package.LastUpdateCheck == null ||
Package.LastUpdateCheck < DateTime.Now.AddMinutes(-15);
if (!canCheckUpdate)
{
return Package.UpdateAvailable;
}
try
{
var hasUpdate = await basePackage.CheckForUpdates(Package);
Package.UpdateAvailable = hasUpdate;
Package.LastUpdateCheck = DateTimeOffset.Now;
settingsManager.SetLastUpdateCheck(Package);
return hasUpdate;
}
catch (Exception e)
{
logger.Error(e, $"Error checking {Package.PackageName} for updates");
return false;
}
}
/// <summary>
/// Deletes a directory and all of its contents recursively.
/// Uses Polly to retry the deletion if it fails, up to 5 times with an exponential backoff.
/// </summary>
/// <param name="targetDirectory"></param>
private Task DeleteDirectoryAsync(string targetDirectory)
{
var policy = Policy.Handle<IOException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)),
onRetry: (exception, calculatedWaitDuration) =>
{
logger.Warn(
exception,
"Deletion of {TargetDirectory} failed. Retrying in {CalculatedWaitDuration}",
targetDirectory, calculatedWaitDuration);
});
return policy.ExecuteAsync(async () =>
{
await Task.Run(() =>
{
DeleteDirectory(targetDirectory);
});
});
}
private void DeleteDirectory(string targetDirectory)
{
// Skip if directory does not exist
if (!Directory.Exists(targetDirectory))
{
return;
}
// For junction points, delete with recursive false
if (new DirectoryInfo(targetDirectory).LinkTarget != null)
{
logger.Info("Removing junction point {TargetDirectory}", targetDirectory);
try
{
Directory.Delete(targetDirectory, false);
return;
}
catch (IOException ex)
{
throw new IOException($"Failed to delete junction point {targetDirectory}", ex);
}
}
// Recursively delete all subdirectories
var subdirectoryEntries = Directory.GetDirectories(targetDirectory);
foreach (var subdirectoryPath in subdirectoryEntries)
{
DeleteDirectory(subdirectoryPath);
}
// Delete all files in the directory
var fileEntries = Directory.GetFiles(targetDirectory);
foreach (var filePath in fileEntries)
{
try
{
File.SetAttributes(filePath, FileAttributes.Normal);
File.Delete(filePath);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete file {filePath}", ex);
}
}
// Delete the target directory itself
try
{
Directory.Delete(targetDirectory, false);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete directory {targetDirectory}", ex);
}
}
}

313
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

@ -1,29 +1,21 @@
using System; using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using Polly;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol; using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -37,53 +29,21 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(PackageManagerPage))] [View(typeof(PackageManagerPage))]
public partial class PackageManagerViewModel : PageViewModelBase public partial class PackageManagerViewModel : PageViewModelBase
{ {
private readonly ILogger<PackageManagerViewModel> logger;
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory; private readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private readonly ServiceManager<ViewModelBase> dialogFactory; private readonly ServiceManager<ViewModelBase> dialogFactory;
private const int MinutesToWaitForUpdateCheck = 60; public PackageManagerViewModel(ISettingsManager settingsManager, IPackageFactory packageFactory,
ServiceManager<ViewModelBase> dialogFactory)
public PackageManagerViewModel(ILogger<PackageManagerViewModel> logger,
ISettingsManager settingsManager, IPackageFactory packageFactory,
INotificationService notificationService, ServiceManager<ViewModelBase> dialogFactory)
{ {
this.logger = logger;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.packageFactory = packageFactory; this.packageFactory = packageFactory;
this.notificationService = notificationService;
this.dialogFactory = dialogFactory; this.dialogFactory = dialogFactory;
Packages = EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged;
new ObservableCollection<InstalledPackage>(settingsManager.Settings.InstalledPackages);
SelectedPackage = Packages.FirstOrDefault();
} }
[ObservableProperty] [ObservableProperty] private ObservableCollection<PackageCardViewModel> packages;
private InstalledPackage? selectedPackage;
[ObservableProperty, NotifyPropertyChangedFor(nameof(ProgressBarVisibility))]
private int progressValue;
[ObservableProperty]
private string progressText = string.Empty;
[ObservableProperty, NotifyPropertyChangedFor(nameof(ProgressBarVisibility))]
private bool isIndeterminate;
[ObservableProperty]
private bool isUninstalling;
[ObservableProperty, NotifyPropertyChangedFor(nameof(SelectedPackage))]
private bool updateAvailable;
[ObservableProperty] private string installButtonText;
[ObservableProperty] private bool installButtonEnabled;
public bool ProgressBarVisibility => ProgressValue > 0 || IsIndeterminate;
public ObservableCollection<InstalledPackage> Packages { get; }
public override bool CanNavigateNext { get; protected set; } = true; public override bool CanNavigateNext { get; protected set; } = true;
public override bool CanNavigatePrevious { get; protected set; } public override bool CanNavigatePrevious { get; protected set; }
@ -92,258 +52,23 @@ public partial class PackageManagerViewModel : PageViewModelBase
public override async Task OnLoadedAsync() public override async Task OnLoadedAsync()
{ {
if (!Design.IsDesignMode) if (Design.IsDesignMode) return;
{
await CheckUpdates();
}
var installedPackages = settingsManager.Settings.InstalledPackages;
SelectedPackage = installedPackages.FirstOrDefault(x =>
x.Id == settingsManager.Settings.ActiveInstalledPackageId);
SelectedPackage ??= installedPackages.FirstOrDefault();
InstallButtonEnabled = SelectedPackage != null;
}
private async Task CheckUpdates()
{
var installedPackages = settingsManager.Settings.InstalledPackages; var installedPackages = settingsManager.Settings.InstalledPackages;
Packages.Clear(); Packages = new ObservableCollection<PackageCardViewModel>(installedPackages.Select(
foreach (var packageToUpdate in installedPackages) package => dialogFactory.Get<PackageCardViewModel>(vm =>
{
var basePackage = packageFactory.FindPackageByName(packageToUpdate.PackageName);
if (basePackage == null) continue;
var canCheckUpdate = packageToUpdate.LastUpdateCheck == null ||
packageToUpdate.LastUpdateCheck.Value.AddMinutes(MinutesToWaitForUpdateCheck) <
DateTimeOffset.Now;
if (canCheckUpdate)
{
var hasUpdate = await basePackage.CheckForUpdates(packageToUpdate);
packageToUpdate.UpdateAvailable = hasUpdate;
packageToUpdate.LastUpdateCheck = DateTimeOffset.Now;
settingsManager.SetLastUpdateCheck(packageToUpdate);
}
Packages.Add(packageToUpdate);
}
}
public async Task Launch()
{
if (SelectedPackage == null) return;
if (InstallButtonText == "Launch")
{
settingsManager.Transaction(s => s.ActiveInstalledPackageId = SelectedPackage.Id);
EventManager.Instance.RequestPageChange(typeof(LaunchPageViewModel));
EventManager.Instance.OnPackageLaunchRequested(SelectedPackage.Id);
}
else
{
await UpdateSelectedPackage();
}
}
public async Task Uninstall()
{
// In design mode, just remove the package from the list
if (Design.IsDesignMode)
{
if (SelectedPackage != null)
{
Packages.Remove(SelectedPackage);
}
return;
}
if (SelectedPackage?.LibraryPath == null)
{
logger.LogError("No package selected to uninstall");
return;
}
var dialog = new ContentDialog
{
Title = "Are you sure?",
Content = "This will delete all folders in the package directory, including any generated images in that directory as well as any files you may have added.",
PrimaryButtonText = "Yes, delete it",
CloseButtonText = "No, keep it",
DefaultButton = ContentDialogButton.Primary
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
IsUninstalling = true;
InstallButtonEnabled = false;
var deleteTask = DeleteDirectoryAsync(Path.Combine(settingsManager.LibraryDir,
SelectedPackage.LibraryPath));
var taskResult = await notificationService.TryAsync(deleteTask,
"Some files could not be deleted. Please close any open files in the package directory and try again.");
if (taskResult.IsSuccessful)
{
notificationService.Show(new Notification("Success",
$"Package {SelectedPackage.DisplayName} uninstalled",
NotificationType.Success));
settingsManager.Transaction(settings =>
{
settings.RemoveInstalledPackageAndUpdateActive(SelectedPackage);
});
}
IsUninstalling = false;
InstallButtonEnabled = true;
await OnLoadedAsync();
}
}
partial void OnSelectedPackageChanged(InstalledPackage? value)
{
if (value is null) return;
UpdateAvailable = value.UpdateAvailable;
InstallButtonText = value.UpdateAvailable ? "Update" : "Launch";
}
/// <summary>
/// Deletes a directory and all of its contents recursively.
/// Uses Polly to retry the deletion if it fails, up to 5 times with an exponential backoff.
/// </summary>
/// <param name="targetDirectory"></param>
private Task DeleteDirectoryAsync(string targetDirectory)
{
var policy = Policy.Handle<IOException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)),
onRetry: (exception, calculatedWaitDuration) =>
{
logger.LogWarning(
exception,
"Deletion of {TargetDirectory} failed. Retrying in {CalculatedWaitDuration}",
targetDirectory, calculatedWaitDuration);
});
return policy.ExecuteAsync(async () =>
{
await Task.Run(() =>
{
DeleteDirectory(targetDirectory);
});
});
}
private void DeleteDirectory(string targetDirectory)
{
// Skip if directory does not exist
if (!Directory.Exists(targetDirectory))
{
return;
}
// For junction points, delete with recursive false
if (new DirectoryInfo(targetDirectory).LinkTarget != null)
{
logger.LogInformation("Removing junction point {TargetDirectory}", targetDirectory);
try
{
Directory.Delete(targetDirectory, false);
return;
}
catch (IOException ex)
{ {
throw new IOException($"Failed to delete junction point {targetDirectory}", ex); vm.Package = package;
} return vm;
} })));
// Recursively delete all subdirectories
var subdirectoryEntries = Directory.GetDirectories(targetDirectory);
foreach (var subdirectoryPath in subdirectoryEntries)
{
DeleteDirectory(subdirectoryPath);
}
// Delete all files in the directory
var fileEntries = Directory.GetFiles(targetDirectory);
foreach (var filePath in fileEntries)
{
try
{
File.SetAttributes(filePath, FileAttributes.Normal);
File.Delete(filePath);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete file {filePath}", ex);
}
}
// Delete the target directory itself
try
{
Directory.Delete(targetDirectory, false);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete directory {targetDirectory}", ex);
}
}
private async Task UpdateSelectedPackage()
{
if (SelectedPackage == null) return;
var installedPackage = SelectedPackage;
var package = packageFactory.FindPackageByName(installedPackage.PackageName);
if (package == null)
{
logger.LogError("Could not find package {SelectedPackagePackageName}",
installedPackage.PackageName);
return;
}
ProgressText = $"Updating {installedPackage.DisplayName} to latest version...";
package.InstallLocation = installedPackage.FullPath!;
var progress = new Progress<ProgressReport>(progress =>
{
var percent = Convert.ToInt32(progress.Percentage);
ProgressValue = percent;
IsIndeterminate = progress.IsIndeterminate;
ProgressText = $"Updating {SelectedPackage.DisplayName} to latest version... {percent:N0}%";
EventManager.Instance.OnGlobalProgressChanged(percent); foreach (var package in Packages)
});
var updateResult = await package.Update(installedPackage, progress);
if (string.IsNullOrWhiteSpace(updateResult))
{ {
var errorMsg = await package.OnLoadedAsync();
$"There was an error updating {installedPackage.DisplayName}. Please try again later.";
if (installedPackage.PackageName == "automatic")
{
errorMsg = errorMsg.Replace("Please try again later.",
"Please stash any changes before updating, or manually update the package.");
}
// there was an error
notificationService.Show(new Notification("Error updating package",
errorMsg, NotificationType.Error));
} }
settingsManager.UpdatePackageVersionNumber(installedPackage.Id, updateResult);
notificationService.Show("Update complete",
$"{installedPackage.DisplayName} has been updated to the latest version.",
NotificationType.Success);
installedPackage.UpdateAvailable = false;
UpdateAvailable = false;
InstallButtonText = "Launch";
await OnLoadedAsync();
await DelayedClearProgress(TimeSpan.FromSeconds(3));
} }
[RelayCommand] public async Task ShowInstallDialog()
private async Task ShowInstallDialog()
{ {
var viewModel = dialogFactory.Get<InstallerViewModel>(); var viewModel = dialogFactory.Get<InstallerViewModel>();
viewModel.AvailablePackages = packageFactory.GetAllAvailablePackages().ToImmutableArray(); viewModel.AvailablePackages = packageFactory.GetAllAvailablePackages().ToImmutableArray();
@ -367,12 +92,6 @@ public partial class PackageManagerViewModel : PageViewModelBase
await OnLoadedAsync(); await OnLoadedAsync();
} }
private async Task DelayedClearProgress(TimeSpan delay) private void OnInstalledPackagesChanged(object? sender, EventArgs e) =>
{ OnLoadedAsync().SafeFireAndForget();
await Task.Delay(delay);
ProgressText = string.Empty;
ProgressValue = 0;
IsIndeterminate = false;
}
} }

2
StabilityMatrix.Avalonia/ViewModels/ProgressViewModel.cs

@ -5,7 +5,7 @@ namespace StabilityMatrix.Avalonia.ViewModels;
/// <summary> /// <summary>
/// Generic view model for progress reporting. /// Generic view model for progress reporting.
/// </summary> /// </summary>
public partial class ProgressViewModel : ObservableObject public partial class ProgressViewModel : ViewModelBase
{ {
[ObservableProperty, NotifyPropertyChangedFor(nameof(IsTextVisible))] [ObservableProperty, NotifyPropertyChangedFor(nameof(IsTextVisible))]
private string? text; private string? text;

278
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml

@ -7,128 +7,190 @@
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:packageManager="clr-namespace:StabilityMatrix.Avalonia.ViewModels.PackageManager"
xmlns:faicon="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="viewModels:PackageManagerViewModel" x:DataType="viewModels:PackageManagerViewModel"
x:CompileBindings="True" x:CompileBindings="True"
d:DataContext="{x:Static designData:DesignData.PackageManagerViewModel}" d:DataContext="{x:Static designData:DesignData.PackageManagerViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.PackageManagerPage"> x:Class="StabilityMatrix.Avalonia.Views.PackageManagerPage">
<Grid Margin="16" RowDefinitions="Auto,*"> <Grid Margin="16" RowDefinitions="Auto,*,Auto">
<StackPanel <!-- Title -->
Grid.Row="0" <TextBlock Grid.Row="0" Text="{Binding Title}"
Orientation="Vertical" Margin="0,0,0,16"
IsVisible="{Binding ProgressBarVisibility, FallbackValue=True}"> FontSize="24"/>
<ProgressBar <!-- Cards -->
IsIndeterminate="{Binding IsIndeterminate, FallbackValue=False}" <ScrollViewer Grid.Row="1">
Maximum="100" <ItemsRepeater
Value="{Binding ProgressValue, FallbackValue=10}" ItemsSource="{Binding Packages}">
Width="500" /> <ItemsRepeater.Layout>
<TextBlock <UniformGridLayout MinColumnSpacing="12" MinRowSpacing="12" />
HorizontalAlignment="Center" </ItemsRepeater.Layout>
Padding="8" <ItemsRepeater.ItemTemplate>
Text="{Binding ProgressText, FallbackValue=Installing...}" /> <DataTemplate DataType="{x:Type packageManager:PackageCardViewModel}">
</StackPanel> <controls:Card Padding="8">
<Grid RowDefinitions="Auto, Auto, Auto, Auto"
ColumnDefinitions="Auto">
<StackPanel <TextBlock Grid.Row="0"
Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Left" Text="{Binding Package.DisplayName}"
Margin="16" TextAlignment="Left" />
Orientation="Horizontal"> <Button
<StackPanel Orientation="Vertical"> Grid.Row="0"
<Button Grid.RowSpan="2"
Command="{Binding ShowInstallDialogCommand}" HorizontalContentAlignment="Right"
Height="40" HorizontalAlignment="Right"
HorizontalAlignment="Stretch" Margin="8,-8,0,4"
Margin="0,0,0,16" Classes="transparent"
Classes="success" Width="24"
VerticalContentAlignment="Center"> BorderThickness="0">
<StackPanel Orientation="Horizontal"> <ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<ui:SymbolIcon Symbol="Add" /> <Button.Flyout>
<Label <MenuFlyout Placement="BottomEdgeAlignedLeft">
Content="Add Package" <MenuItem Header="Check for Update"
Margin="8,0,0,0" Command="{Binding OnLoadedAsync}">
VerticalContentAlignment="Center" /> <MenuItem.Icon>
</StackPanel> <ui:SymbolIcon Symbol="Refresh" />
</Button> </MenuItem.Icon>
</MenuItem>
<MenuItem Header="Open in Explorer"
Command="{Binding OpenFolder}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="OpenFolder" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Uninstall"
Command="{Binding Uninstall}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Delete" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
<ListBox <TextBlock Grid.Row="1"
ItemsSource="{Binding Packages}" Grid.Column="0"
SelectedItem="{Binding SelectedPackage, Mode=TwoWay}" Margin="2,0,0,0"
IsVisible="{Binding !!Packages.Count}"> VerticalAlignment="Center"
Text="{Binding InstalledVersion}" />
<ListBox.Template> <controls:BetterAdvancedImage
<ControlTemplate> Grid.Row="2"
<Border Grid.Column="0"
BorderBrush="#FFCCCCCC" Margin="0,8,0,8"
BorderThickness="1" Height="160"
CornerRadius="5"> Width="200"
<ItemsPresenter /> CornerRadius="4"
</Border> HorizontalAlignment="Center"
</ControlTemplate> VerticalContentAlignment="Top"
</ListBox.Template> HorizontalContentAlignment="Center"
Source="{Binding CardImage}"
Stretch="UniformToFill" />
<ListBox.ItemTemplate> <Grid Grid.Row="3" Grid.Column="0"
<DataTemplate DataType="{x:Type models:InstalledPackage}"> IsVisible="{Binding IsUpdateAvailable}"
<StackPanel Margin="8" VerticalAlignment="Top"> ColumnDefinitions="*, *">
<TextBlock Margin="0,5,0,5" Text="{Binding DisplayName}" /> <Button Grid.Column="0" Classes="accent"
<TextBlock Margin="0,0,0,5" Text="{Binding DisplayVersion}" /> VerticalAlignment="Bottom"
</StackPanel> HorizontalAlignment="Stretch"
</DataTemplate> Command="{Binding Launch}">
</ListBox.ItemTemplate> <StackPanel Orientation="Horizontal" Margin="0,2,0,2">
</ListBox> <faicon:Icon Value="fa-solid fa-rocket"
</StackPanel> Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="Launch" />
</StackPanel>
</Button>
<Button Grid.Column="1" Classes="accent"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Command="{Binding Update}">
<StackPanel Orientation="Horizontal" Margin="0,2,0,2">
<faicon:Icon Value="fa-solid fa-download"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="Update" />
</StackPanel>
</Button>
</Grid>
<!-- Big launch button -->
<Button Grid.Row="3" Grid.Column="0" Classes="accent"
VerticalAlignment="Bottom"
IsVisible="{Binding !IsUpdateAvailable}"
Command="{Binding Launch}"
HorizontalAlignment="Stretch">
<StackPanel Orientation="Horizontal" Margin="0,2,0,2">
<faicon:Icon Value="fa-solid fa-rocket"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="Launch" />
</StackPanel>
</Button>
<StackPanel Margin="32,0,0,0" Orientation="Vertical"> <!-- Update overlay -->
<TextBlock <Rectangle
FontSize="24" Grid.Row="0" Grid.RowSpan="4"
FontWeight="Bold" Grid.Column="0"
Text="{Binding SelectedPackage.DisplayName, Fill="#DD000000"
FallbackValue='Click &quot;Add Package&quot; to install a package'}" /> HorizontalAlignment="Stretch"
<TextBlock FontSize="12" Margin="0,5,0,5"> VerticalAlignment="Stretch"
<Run Text="{Binding SelectedPackage.PackageName, FallbackValue=''}" /> IsVisible="{Binding IsProgressVisible}" />
<Run Text="{Binding SelectedPackage.DisplayVersion, FallbackValue=''}" /> <Grid Grid.Row="0" Grid.RowSpan="4"
</TextBlock> Grid.Column="0"
<TextBlock HorizontalAlignment="Stretch"
FontSize="12" VerticalAlignment="Center"
Margin="0,5,0,5" RowDefinitions="Auto, *"
Text="{Binding SelectedPackage.FullPath, FallbackValue=''}" /> IsVisible="{Binding IsProgressVisible}">
<controls:ProgressRing
HorizontalAlignment="Center"
IsIndeterminate="{Binding IsIndeterminate}"
Width="120"
Height="120"
StartAngle="90"
EndAngle="450"
Value="{Binding Value}"
VerticalAlignment="Center" />
<TextBlock Grid.Row="1"
HorizontalAlignment="Center"
Margin="8,8,8,0"
TextAlignment="Center"
Width="180"
Text="{Binding Text, TargetNullValue=Updating so and so to the latest version... 5%}"
TextWrapping="Wrap"
VerticalAlignment="Center" />
</Grid>
</Grid>
</controls:Card>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
<UniformGrid <!-- Teaching Tip -->
Margin="0,16,0,0" <ui:TeachingTip Grid.Row="0" Name="TeachingTip1"
Columns="2" Target="{Binding #AddPackagesButton}"
MinHeight="48" Title="Add a package to get started!"
MinWidth="208" PreferredPlacement="Top"
HorizontalAlignment="Left"> IsOpen="{Binding !Packages.Count}"/>
<Button
Command="{Binding Launch}"
Content="{Binding InstallButtonText}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="0,0,8,0"
IsEnabled="{Binding InstallButtonEnabled}"
IsVisible="{Binding InstallButtonEnabled}"
Classes="success"/>
<Button
Command="{Binding Uninstall}"
Content="Uninstall"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="8,0,0,0"
Classes="danger"
IsEnabled="{Binding !IsUninstalling}"
IsVisible="{Binding InstallButtonEnabled}"/>
</UniformGrid>
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsUninstalling}"> <!-- Add Packages Button -->
<controls:ProgressRing Height="24" Width="24" <Button Grid.Row="2"
Margin="8, 16, 8, 8" Classes="transparent"
VerticalAlignment="Center" VerticalAlignment="Bottom"
BorderThickness="4" Name="AddPackagesButton"
IsIndeterminate="True" HorizontalAlignment="Left" /> Margin="0, 8, 0, 0"
<TextBlock Text="Uninstalling..." VerticalAlignment="Center" Margin="0,8,0,0" /> HorizontalAlignment="Stretch"
</StackPanel> Command="{Binding ShowInstallDialog}">
<StackPanel Orientation="Horizontal" Margin="8">
<ui:SymbolIcon FontSize="18" Symbol="Add" />
<TextBlock Margin="4,0,0,0" Text="Add Package" />
</StackPanel> </StackPanel>
</StackPanel> </Button>
</Grid> </Grid>
</controls:UserControlBase> </controls:UserControlBase>

1
StabilityMatrix.Core/Database/LiteDbContext.cs

@ -115,7 +115,6 @@ public class LiteDbContext : ILiteDbContext
public void Dispose() public void Dispose()
{ {
database?.UnderlyingDatabase.Dispose();
database?.Dispose(); database?.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }

2
StabilityMatrix.Core/Models/InstalledPackage.cs

@ -38,7 +38,7 @@ public class InstalledPackage
public List<LaunchOption>? LaunchArgs { get; set; } public List<LaunchOption>? LaunchArgs { get; set; }
public DateTimeOffset? LastUpdateCheck { get; set; } public DateTimeOffset? LastUpdateCheck { get; set; }
[JsonIgnore] public bool UpdateAvailable { get; set; } public bool UpdateAvailable { get; set; }
/// <summary> /// <summary>
/// Get the path as a relative sub-path of the relative path. /// Get the path as a relative sub-path of the relative path.

5
StabilityMatrix.Core/Services/SettingsManager.cs

@ -339,7 +339,6 @@ public class SettingsManager : ISettingsManager
} }
package.PackageVersion = newVersion; package.PackageVersion = newVersion;
package.DisplayVersion = string.IsNullOrWhiteSpace(package.InstalledBranch) package.DisplayVersion = string.IsNullOrWhiteSpace(package.InstalledBranch)
? newVersion ? newVersion
: $"{package.InstalledBranch}@{newVersion[..7]}"; : $"{package.InstalledBranch}@{newVersion[..7]}";
@ -349,7 +348,9 @@ public class SettingsManager : ISettingsManager
public void SetLastUpdateCheck(InstalledPackage package) public void SetLastUpdateCheck(InstalledPackage package)
{ {
Settings.InstalledPackages.First(p => p.DisplayName == package.DisplayName).LastUpdateCheck = package.LastUpdateCheck; var installedPackage = Settings.InstalledPackages.First(p => p.DisplayName == package.DisplayName);
installedPackage.LastUpdateCheck = package.LastUpdateCheck;
installedPackage.UpdateAvailable = package.UpdateAvailable;
SaveSettings(); SaveSettings();
} }

Loading…
Cancel
Save