Browse Source

Merge branch 'main' of https://github.com/ionite34/StabilityMatrix into select-model-version-view-update

pull/109/head
JT 1 year ago
parent
commit
d63c670477
  1. 1
      CHANGELOG.md
  2. 15
      StabilityMatrix.Avalonia/App.axaml.cs
  3. 21
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  4. 91
      StabilityMatrix.Avalonia/Extensions/DirectoryPathExtensions.cs
  5. 63
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  6. 21
      StabilityMatrix.Avalonia/Languages/Resources.resx
  7. 92
      StabilityMatrix.Avalonia/Services/ServiceManager.cs
  8. 1
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  9. 221
      StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs
  10. 171
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  11. 112
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  12. 51
      StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml
  13. 17
      StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml.cs
  14. 55
      StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml
  15. 24
      StabilityMatrix.Core/Helper/PropertyComparer.cs
  16. 3
      StabilityMatrix.Core/Models/InstalledPackage.cs
  17. 4
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  18. 105
      StabilityMatrix.Core/Models/Packages/UnknownPackage.cs
  19. 17
      StabilityMatrix.Core/Models/UnknownInstalledPackage.cs
  20. 4
      StabilityMatrix.Tests/Avalonia/DesignDataTests.cs

1
CHANGELOG.md

@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
- Added "Skip to First/Last Page" buttons to the Model Browser
- Added VAE as a checkpoint category in the Model Browser
- Pause/Resume/Cancel buttons on downloads popup. Paused downloads persists and may be resumed after restarting the app
- Unknown Package installs in the Package directory will now show up with a button to import them
### Fixed
- Fixed issue where model version wouldn't be selected in the "All Versions" section of the Model Browser
- Improved Checkpoints page indexing performance

15
StabilityMatrix.Avalonia/App.axaml.cs

@ -247,14 +247,16 @@ public sealed class App : Application
services.AddTransient<LaunchOptionsViewModel>();
services.AddTransient<ExceptionViewModel>();
services.AddTransient<EnvVarsViewModel>();
services.AddTransient<PackageImportViewModel>();
// Dialog view models (singleton)
services.AddSingleton<FirstLaunchSetupViewModel>();
services.AddSingleton<UpdateViewModel>();
// Other transients (usually sub view models)
services.AddTransient<CheckpointFolder>()
.AddTransient<CheckpointFile>()
.AddTransient<CheckpointBrowserCardViewModel>();
services.AddTransient<CheckpointFolder>();
services.AddTransient<CheckpointFile>();
services.AddTransient<CheckpointBrowserCardViewModel>();
services.AddTransient<PackageCardViewModel>();
// Global progress
@ -280,7 +282,9 @@ public sealed class App : Application
.Register(provider.GetRequiredService<ExceptionViewModel>)
.Register(provider.GetRequiredService<EnvVarsViewModel>)
.Register(provider.GetRequiredService<ProgressManagerViewModel>)
.Register(provider.GetRequiredService<FirstLaunchSetupViewModel>));
.Register(provider.GetRequiredService<FirstLaunchSetupViewModel>)
.Register(provider.GetRequiredService<PackageImportViewModel>)
);
}
internal static void ConfigureViews(IServiceCollection services)
@ -300,6 +304,7 @@ public sealed class App : Application
services.AddTransient<UpdateDialog>();
services.AddTransient<ExceptionDialog>();
services.AddTransient<EnvVarsDialog>();
services.AddTransient<PackageImportDialog>();
// Controls
services.AddTransient<RefreshBadge>();

21
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -4,7 +4,6 @@ using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Models;
@ -12,8 +11,8 @@ using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Helper;
@ -25,8 +24,6 @@ using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
using CheckpointFile = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFile;
using CheckpointFolder = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFolder;
namespace StabilityMatrix.Avalonia.DesignData;
@ -296,10 +293,15 @@ public static class DesignData
{
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;
vm.SetPackages(settings.Settings.InstalledPackages);
vm.SetUnknownPackages(new InstalledPackage[]
{
UnknownInstalledPackage.FromDirectoryName("sd-unknown"),
});
vm.PackageCards[0].IsUpdateAvailable = true;
return vm;
}
}
@ -409,6 +411,9 @@ The gallery images are often inpainted, but you will get something very similar
};
});
public static PackageImportViewModel PackageImportViewModel =>
DialogFactory.Get<PackageImportViewModel>();
public static RefreshBadgeViewModel RefreshBadgeViewModel => new()
{
State = ProgressState.Success

91
StabilityMatrix.Avalonia/Extensions/DirectoryPathExtensions.cs

@ -0,0 +1,91 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Polly;
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Avalonia.Extensions;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class DirectoryPathExtensions
{
/// <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>
public static Task DeleteVerboseAsync(this DirectoryPath directory, ILogger? logger = default)
{
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}",
directory, calculatedWaitDuration);
});
return policy.ExecuteAsync(async () =>
{
await Task.Run(() => { DeleteVerbose(directory, logger); });
});
}
/// <summary>
/// Deletes a directory and all of its contents recursively.
/// Removes link targets without deleting the source.
/// </summary>
public static void DeleteVerbose(this DirectoryPath directory, ILogger? logger = default)
{
// Skip if directory does not exist
if (!directory.Exists)
{
return;
}
// For junction points, delete with recursive false
if (directory.IsSymbolicLink)
{
logger?.LogInformation("Removing junction point {TargetDirectory}", directory);
try
{
directory.Delete(false);
return;
}
catch (IOException ex)
{
throw new IOException($"Failed to delete junction point {directory}", ex);
}
}
// Recursively delete all subdirectories
foreach (var subDir in directory.Info.EnumerateDirectories())
{
DeleteVerbose(subDir, logger);
}
// Delete all files in the directory
foreach (var filePath in directory.Info.EnumerateFiles())
{
try
{
filePath.Attributes = FileAttributes.Normal;
filePath.Delete();
}
catch (IOException ex)
{
throw new IOException($"Failed to delete file {filePath.FullName}", ex);
}
}
// Delete this directory
try
{
directory.Delete(false);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete directory {directory}", ex);
}
}
}

63
StabilityMatrix.Avalonia/Languages/Resources.Designer.cs generated

@ -68,6 +68,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Import.
/// </summary>
public static string Action_Import {
get {
return ResourceManager.GetString("Action_Import", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch.
/// </summary>
@ -113,6 +122,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Branches.
/// </summary>
public static string Label_Branches {
get {
return ResourceManager.GetString("Label_Branches", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Language.
/// </summary>
@ -122,6 +140,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Package Type.
/// </summary>
public static string Label_PackageType {
get {
return ResourceManager.GetString("Label_PackageType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Relaunch Required.
/// </summary>
@ -131,6 +158,42 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Releases.
/// </summary>
public static string Label_Releases {
get {
return ResourceManager.GetString("Label_Releases", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unknown Package.
/// </summary>
public static string Label_UnknownPackage {
get {
return ResourceManager.GetString("Label_UnknownPackage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Version.
/// </summary>
public static string Label_Version {
get {
return ResourceManager.GetString("Label_Version", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Version Type.
/// </summary>
public static string Label_VersionType {
get {
return ResourceManager.GetString("Label_VersionType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Relaunch is required for new language option to take effect.
/// </summary>

21
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -45,4 +45,25 @@
<data name="Label_RelaunchRequired" xml:space="preserve">
<value>Relaunch Required</value>
</data>
<data name="Label_UnknownPackage" xml:space="preserve">
<value>Unknown Package</value>
</data>
<data name="Action_Import" xml:space="preserve">
<value>Import</value>
</data>
<data name="Label_PackageType" xml:space="preserve">
<value>Package Type</value>
</data>
<data name="Label_Version" xml:space="preserve">
<value>Version</value>
</data>
<data name="Label_VersionType" xml:space="preserve">
<value>Version Type</value>
</data>
<data name="Label_Releases" xml:space="preserve">
<value>Releases</value>
</data>
<data name="Label_Branches" xml:space="preserve">
<value>Branches</value>
</data>
</root>

92
StabilityMatrix.Avalonia/Services/ServiceManager.cs

@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia.Services;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class ServiceManager<T>
{
// Holds providers
@ -111,6 +115,48 @@ public class ServiceManager<T>
$"Service of type {typeof(TService)} is not registered for {typeof(T)}");
}
/// <summary>
/// Get a view model instance from runtime type
/// </summary>
[SuppressMessage("ReSharper", "InconsistentlySynchronizedField")]
public T Get(Type serviceType)
{
if (!serviceType.IsAssignableFrom(typeof(T)))
{
throw new ArgumentException(
$"Service type {serviceType} is not assignable from {typeof(T)}");
}
if (instances.TryGetValue(serviceType, out var instance))
{
if (instance is null)
{
throw new ArgumentException(
$"Service of type {serviceType} was registered as null");
}
return (T) instance;
}
if (providers.TryGetValue(serviceType, out var provider))
{
if (provider is null)
{
throw new ArgumentException(
$"Service of type {serviceType} was registered as null");
}
var result = provider();
if (result is null)
{
throw new ArgumentException(
$"Service provider for type {serviceType} returned null");
}
return (T) result;
}
throw new ArgumentException(
$"Service of type {serviceType} is not registered for {typeof(T)}");
}
/// <summary>
/// Get a view model instance with an initializer parameter
/// </summary>
@ -129,4 +175,50 @@ public class ServiceManager<T>
initializer(instance);
return instance;
}
/// <summary>
/// Get a view model instance, set as DataContext of its View, and return
/// a BetterContentDialog with that View as its Content
/// </summary>
public BetterContentDialog GetDialog<TService>() where TService : T
{
var instance = Get<TService>()!;
if (Attribute.GetCustomAttribute(instance.GetType(), typeof(ViewAttribute)) is not ViewAttribute
viewAttr)
{
throw new InvalidOperationException($"View not found for {instance.GetType().FullName}");
}
if (Activator.CreateInstance(viewAttr.GetViewType()) is not Control view)
{
throw new NullReferenceException($"Unable to create instance for {instance.GetType().FullName}");
}
return new BetterContentDialog { Content = view };
}
/// <summary>
/// Get a view model instance with initializer, set as DataContext of its View, and return
/// a BetterContentDialog with that View as its Content
/// </summary>
public BetterContentDialog GetDialog<TService>(Action<TService> initializer) where TService : T
{
var instance = Get(initializer)!;
if (Attribute.GetCustomAttribute(instance.GetType(), typeof(ViewAttribute)) is not ViewAttribute
viewAttr)
{
throw new InvalidOperationException($"View not found for {instance.GetType().FullName}");
}
if (Activator.CreateInstance(viewAttr.GetViewType()) is not Control view)
{
throw new NullReferenceException($"Unable to create instance for {instance.GetType().FullName}");
}
view.DataContext = instance;
return new BetterContentDialog { Content = view };
}
}

1
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -26,6 +26,7 @@
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="DynamicData" Version="7.14.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.0" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.207" />
<PackageReference Include="FluentIcons.FluentAvalonia" Version="1.1.207" />

221
StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs

@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using NLog;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(PackageImportDialog))]
public partial class PackageImportViewModel : ContentDialogViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly IPackageFactory packageFactory;
private readonly ISettingsManager settingsManager;
[ObservableProperty] private DirectoryPath? packagePath;
[ObservableProperty] private BasePackage? selectedBasePackage;
public IReadOnlyList<BasePackage> AvailablePackages
=> packageFactory.GetAllAvailablePackages().ToImmutableArray();
[ObservableProperty] private PackageVersion? selectedVersion;
[ObservableProperty] private ObservableCollection<GitCommit>? availableCommits;
[ObservableProperty] private ObservableCollection<PackageVersion>? availableVersions;
[ObservableProperty] private GitCommit? selectedCommit;
// Version types (release or commit)
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ReleaseLabelText),
nameof(IsReleaseMode), nameof(SelectedVersion))]
private PackageVersionType selectedVersionType = PackageVersionType.Commit;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsReleaseModeAvailable))]
private PackageVersionType availableVersionTypes =
PackageVersionType.GithubRelease | PackageVersionType.Commit;
public string ReleaseLabelText => IsReleaseMode ? "Version" : "Branch";
public bool IsReleaseMode
{
get => SelectedVersionType == PackageVersionType.GithubRelease;
set => SelectedVersionType = value ? PackageVersionType.GithubRelease : PackageVersionType.Commit;
}
public bool IsReleaseModeAvailable => AvailableVersionTypes.HasFlag(PackageVersionType.GithubRelease);
public PackageImportViewModel(
IPackageFactory packageFactory,
ISettingsManager settingsManager)
{
this.packageFactory = packageFactory;
this.settingsManager = settingsManager;
}
public override async Task OnLoadedAsync()
{
SelectedBasePackage ??= AvailablePackages[0];
if (Design.IsDesignMode) return;
// Populate available versions
try
{
if (IsReleaseMode)
{
var versions = (await SelectedBasePackage.GetAllVersions()).ToList();
AvailableVersions = new ObservableCollection<PackageVersion>(versions);
if (!AvailableVersions.Any()) return;
SelectedVersion = AvailableVersions[0];
}
else
{
var branches = (await SelectedBasePackage.GetAllBranches()).ToList();
AvailableVersions = new ObservableCollection<PackageVersion>(branches.Select(b =>
new PackageVersion
{
TagName = b.Name,
ReleaseNotesMarkdown = b.Commit.Label
}));
UpdateSelectedVersionToLatestMain();
}
}
catch (Exception e)
{
Logger.Warn("Error getting versions: {Exception}", e.ToString());
}
}
private static string GetDisplayVersion(string version, string? branch)
{
return branch == null ? version : $"{branch}@{version[..7]}";
}
// When available version types change, reset selected version type if not compatible
partial void OnAvailableVersionTypesChanged(PackageVersionType value)
{
if (!value.HasFlag(SelectedVersionType))
{
SelectedVersionType = value;
}
}
// When changing branch / release modes, refresh
// ReSharper disable once UnusedParameterInPartialMethod
partial void OnSelectedVersionTypeChanged(PackageVersionType value)
=> OnSelectedBasePackageChanged(SelectedBasePackage);
partial void OnSelectedBasePackageChanged(BasePackage? value)
{
if (value is null || SelectedBasePackage is null)
{
AvailableVersions?.Clear();
AvailableCommits?.Clear();
return;
}
AvailableVersions?.Clear();
AvailableCommits?.Clear();
AvailableVersionTypes = SelectedBasePackage.ShouldIgnoreReleases
? PackageVersionType.Commit
: PackageVersionType.GithubRelease | PackageVersionType.Commit;
if (Design.IsDesignMode) return;
Dispatcher.UIThread.InvokeAsync(async () =>
{
Logger.Debug($"Release mode: {IsReleaseMode}");
var versions = (await value.GetAllVersions(IsReleaseMode)).ToList();
if (!versions.Any()) return;
AvailableVersions = new ObservableCollection<PackageVersion>(versions);
Logger.Debug($"Available versions: {string.Join(", ", AvailableVersions)}");
SelectedVersion = AvailableVersions[0];
if (!IsReleaseMode)
{
var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList();
if (commits is null || commits.Count == 0) return;
AvailableCommits = new ObservableCollection<GitCommit>(commits);
SelectedCommit = AvailableCommits[0];
UpdateSelectedVersionToLatestMain();
}
}).SafeFireAndForget();
}
private void UpdateSelectedVersionToLatestMain()
{
if (AvailableVersions is null)
{
SelectedVersion = null;
}
else
{
// First try to find master
var version = AvailableVersions.FirstOrDefault(x => x.TagName == "master");
// If not found, try main
version ??= AvailableVersions.FirstOrDefault(x => x.TagName == "main");
// If still not found, just use the first one
version ??= AvailableVersions[0];
SelectedVersion = version;
}
}
public void AddPackageWithCurrentInputs()
{
if (SelectedBasePackage is null || PackagePath is null)
return;
string version;
if (IsReleaseMode)
{
version = SelectedVersion?.TagName ??
throw new NullReferenceException("Selected version is null");
}
else
{
version = SelectedCommit?.Sha ??
throw new NullReferenceException("Selected commit is null");
}
var branch = SelectedVersionType == PackageVersionType.GithubRelease ?
null : SelectedVersion!.TagName;
var package = new InstalledPackage
{
Id = Guid.NewGuid(),
DisplayName = PackagePath.Name,
PackageName = SelectedBasePackage.Name,
LibraryPath = $"Packages{Path.DirectorySeparatorChar}{PackagePath.Name}",
PackageVersion = version,
DisplayVersion = GetDisplayVersion(version, branch),
InstalledBranch = branch,
LaunchCommand = SelectedBasePackage.LaunchCommand,
LastUpdateCheck = DateTimeOffset.Now,
};
settingsManager.Transaction(s => s.InstalledPackages.Add(package));
}
}

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

@ -1,20 +1,22 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using NLog;
using Polly;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
@ -23,27 +25,33 @@ namespace StabilityMatrix.Avalonia.ViewModels.PackageManager;
public partial class PackageCardViewModel : ProgressViewModel
{
private readonly ILogger<PackageCardViewModel> logger;
private readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager;
private readonly INavigationService navigationService;
private readonly Logger logger = LogManager.GetCurrentClassLogger();
private readonly ServiceManager<ViewModelBase> vmFactory;
[ObservableProperty] private InstalledPackage? package;
[ObservableProperty] private Uri cardImage;
[ObservableProperty] private string? cardImageSource;
[ObservableProperty] private bool isUpdateAvailable;
[ObservableProperty] private string installedVersion;
[ObservableProperty] private string? installedVersion;
[ObservableProperty] private bool isUnknownPackage;
public PackageCardViewModel(
ILogger<PackageCardViewModel> logger,
IPackageFactory packageFactory,
INotificationService notificationService,
ISettingsManager settingsManager,
INavigationService navigationService)
INavigationService navigationService,
ServiceManager<ViewModelBase> vmFactory)
{
this.logger = logger;
this.packageFactory = packageFactory;
this.notificationService = notificationService;
this.settingsManager = settingsManager;
this.navigationService = navigationService;
this.vmFactory = vmFactory;
}
partial void OnPackageChanged(InstalledPackage? value)
@ -51,10 +59,22 @@ public partial class PackageCardViewModel : ProgressViewModel
if (string.IsNullOrWhiteSpace(value?.PackageName))
return;
if (value.PackageName == UnknownPackage.Key)
{
IsUnknownPackage = true;
CardImageSource = "";
InstalledVersion = "Unknown";
}
else
{
IsUnknownPackage = false;
var basePackage = packageFactory[value.PackageName];
CardImage = basePackage?.PreviewImageUri ?? Assets.NoImage;
CardImageSource = basePackage?.PreviewImageUri.ToString()
?? Assets.NoImage.ToString();
InstalledVersion = value.DisplayVersion ?? "Unknown";
}
}
public override async Task OnLoadedAsync()
{
@ -95,8 +115,9 @@ public partial class PackageCardViewModel : ProgressViewModel
IsIndeterminate = true;
Value = -1;
var deleteTask = DeleteDirectoryAsync(Path.Combine(settingsManager.LibraryDir,
Package.LibraryPath));
var packagePath = new DirectoryPath(settingsManager.LibraryDir, Package.LibraryPath);
var deleteTask = packagePath.DeleteVerboseAsync(logger);
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)
@ -105,10 +126,13 @@ public partial class PackageCardViewModel : ProgressViewModel
$"Package {Package.DisplayName} uninstalled",
NotificationType.Success));
if (!IsUnknownPackage)
{
settingsManager.Transaction(settings =>
{
settings.RemoveInstalledPackageAndUpdateActive(Package);
});
}
EventManager.Instance.OnInstalledPackagesChanged();
}
@ -117,12 +141,12 @@ public partial class PackageCardViewModel : ProgressViewModel
public async Task Update()
{
if (Package == null) return;
if (Package is null || IsUnknownPackage) return;
var basePackage = packageFactory[Package.PackageName!];
if (basePackage == null)
{
logger.Warn("Could not find package {SelectedPackagePackageName}",
logger.LogWarning("Could not find package {SelectedPackagePackageName}",
Package.PackageName);
notificationService.Show("Invalid Package type",
$"Package {Package.PackageName.ToRepr()} is not a valid package type",
@ -130,12 +154,14 @@ public partial class PackageCardViewModel : ProgressViewModel
return;
}
Text = $"Updating {Package.DisplayName}";
var packageName = Package.DisplayName ?? Package.PackageName ?? "";
Text = $"Updating {packageName}";
IsIndeterminate = true;
var progressId = Guid.NewGuid();
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
Package.DisplayName,
Package.DisplayName ?? Package.PackageName!,
new ProgressReport(0f, isIndeterminate: true, type: ProgressType.Update)));
try
@ -152,7 +178,7 @@ public partial class PackageCardViewModel : ProgressViewModel
EventManager.Instance.OnGlobalProgressChanged(percent);
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
Package.DisplayName, progress));
packageName, progress));
});
var updateResult = await basePackage.Update(Package, progress);
@ -170,15 +196,15 @@ public partial class PackageCardViewModel : ProgressViewModel
InstalledVersion = Package.DisplayVersion ?? "Unknown";
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
Package.DisplayName,
packageName,
new ProgressReport(1f, "Update complete", type: ProgressType.Update)));
}
catch (Exception e)
{
logger.Error(e, "Error Updating Package ({PackageName})", basePackage.Name);
logger.LogError(e, "Error Updating Package ({PackageName})", basePackage.Name);
notificationService.ShowPersistent($"Error Updating {Package.DisplayName}", e.Message, NotificationType.Error);
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId,
Package.DisplayName,
packageName,
new ProgressReport(0f, "Update failed", type: ProgressType.Update), Failed: true));
}
finally
@ -189,6 +215,29 @@ public partial class PackageCardViewModel : ProgressViewModel
}
}
public async Task Import()
{
if (!IsUnknownPackage || Design.IsDesignMode) return;
PackageImportViewModel viewModel = null!;
var dialog = vmFactory.GetDialog<PackageImportViewModel>(vm =>
{
vm.PackagePath = new DirectoryPath(Package?.FullPath ?? throw new InvalidOperationException());
viewModel = vm;
});
dialog.PrimaryButtonText = Resources.Action_Import;
dialog.CloseButtonText = Resources.Action_Cancel;
dialog.DefaultButton = ContentDialogButton.Primary;
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
viewModel.AddPackageWithCurrentInputs();
EventManager.Instance.OnInstalledPackagesChanged();
}
}
public async Task OpenFolder()
{
if (string.IsNullOrWhiteSpace(Package?.FullPath))
@ -199,7 +248,7 @@ public partial class PackageCardViewModel : ProgressViewModel
private async Task<bool> HasUpdate()
{
if (Package == null)
if (Package == null || IsUnknownPackage || Design.IsDesignMode)
return false;
var basePackage = packageFactory[Package.PackageName!];
@ -224,86 +273,8 @@ public partial class PackageCardViewModel : ProgressViewModel
}
catch (Exception e)
{
logger.Error(e, $"Error checking {Package.PackageName} for updates");
logger.LogError(e, "Error checking {PackageName} for updates", Package.PackageName);
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);
}
}
}

112
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
@ -17,6 +19,8 @@ using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -34,39 +38,73 @@ public partial class PackageManagerViewModel : PageViewModelBase
private readonly IPackageFactory packageFactory;
private readonly ServiceManager<ViewModelBase> dialogFactory;
public PackageManagerViewModel(ISettingsManager settingsManager, IPackageFactory packageFactory,
ServiceManager<ViewModelBase> dialogFactory)
public override string Title => "Packages";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Box, IsFilled = true };
/// <summary>
/// List of installed packages
/// </summary>
private readonly SourceCache<InstalledPackage, Guid> installedPackages = new(p => p.Id);
/// <summary>
/// List of indexed packages without a corresponding installed package
/// </summary>
private readonly SourceCache<InstalledPackage, Guid> unknownInstalledPackages = new(p => p.Id);
public IObservableCollection<InstalledPackage> Packages { get; } =
new ObservableCollectionExtended<InstalledPackage>();
public IObservableCollection<PackageCardViewModel> PackageCards { get; } =
new ObservableCollectionExtended<PackageCardViewModel>();
public PackageManagerViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
ServiceManager<ViewModelBase> dialogFactory
)
{
this.settingsManager = settingsManager;
this.packageFactory = packageFactory;
this.dialogFactory = dialogFactory;
EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged;
}
[ObservableProperty] private ObservableCollection<PackageCardViewModel> packages;
public override bool CanNavigateNext { get; protected set; } = true;
public override bool CanNavigatePrevious { get; protected set; }
public override string Title => "Packages";
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Box, IsFilled = true};
var installed = installedPackages.Connect();
var unknown = unknownInstalledPackages.Connect();
public override async Task OnLoadedAsync()
installed
.Or(unknown)
.DeferUntilLoaded()
.Bind(Packages)
.Transform(p => dialogFactory.Get<PackageCardViewModel>(vm =>
{
if (Design.IsDesignMode) return;
vm.Package = p;
vm.OnLoadedAsync().SafeFireAndForget();
}))
.Bind(PackageCards)
.Subscribe();
}
var installedPackages = settingsManager.Settings.InstalledPackages;
Packages = new ObservableCollection<PackageCardViewModel>(installedPackages.Select(
package => dialogFactory.Get<PackageCardViewModel>(vm =>
public void SetPackages(IEnumerable<InstalledPackage> packages)
{
vm.Package = package;
return vm;
})));
installedPackages.Edit(s => s.Load(packages));
}
foreach (var package in Packages)
public void SetUnknownPackages(IEnumerable<InstalledPackage> packages)
{
await package.OnLoadedAsync();
unknownInstalledPackages.Edit(s => s.Load(packages));
}
public override async Task OnLoadedAsync()
{
if (Design.IsDesignMode)
return;
installedPackages.EditDiff(settingsManager.Settings.InstalledPackages, InstalledPackage.Comparer);
var currentUnknown = await Task.Run(IndexUnknownPackages);
unknownInstalledPackages.Edit(s => s.Load(currentUnknown));
}
public async Task ShowInstallDialog()
@ -83,16 +121,40 @@ public partial class PackageManagerViewModel : PageViewModelBase
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
Content = new InstallerDialog
{
DataContext = viewModel
}
Content = new InstallerDialog { DataContext = viewModel }
};
await dialog.ShowAsync();
await OnLoadedAsync();
}
private IEnumerable<UnknownInstalledPackage> IndexUnknownPackages()
{
var packageDir = new DirectoryPath(settingsManager.LibraryDir).JoinDir("Packages");
if (!packageDir.Exists)
{
yield break;
}
var currentPackages = settingsManager.Settings.InstalledPackages.ToImmutableArray();
foreach (var subDir in packageDir.Info
.EnumerateDirectories()
.Select(info => new DirectoryPath(info)))
{
var expectedLibraryPath = $"Packages{Path.DirectorySeparatorChar}{subDir.Name}";
// Skip if the package is already installed
if (currentPackages.Any(p => p.LibraryPath == expectedLibraryPath))
{
continue;
}
yield return UnknownInstalledPackage.FromDirectoryName(subDir.Name);
}
}
private void OnInstalledPackagesChanged(object? sender, EventArgs e) =>
OnLoadedAsync().SafeFireAndForget();
}

51
StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml

@ -0,0 +1,51 @@
<controls:UserControlBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:vmDialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static mocks:DesignData.PackageImportViewModel}"
x:DataType="vmDialogs:PackageImportViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.PackageImportDialog">
<StackPanel Margin="8" Spacing="8">
<ui:SettingsExpander Header="{x:Static lang:Resources.Label_PackageType}">
<ui:SettingsExpander.Footer>
<ui:FAComboBox
HorizontalAlignment="Stretch"
ItemsSource="{Binding AvailablePackages}"
DisplayMemberBinding="{Binding DisplayName}"
SelectedItem="{Binding SelectedBasePackage}"/>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Header="{x:Static lang:Resources.Label_VersionType}">
<ui:SettingsExpander.Footer>
<ToggleSwitch
IsEnabled="{Binding IsReleaseModeAvailable}"
OnContent="{x:Static lang:Resources.Label_Releases}"
OffContent="{x:Static lang:Resources.Label_Branches}"
IsChecked="{Binding IsReleaseMode}"/>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Header="{x:Static lang:Resources.Label_Version}">
<ui:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal" Spacing="8">
<ui:FAComboBox
ItemsSource="{Binding AvailableVersions}"
DisplayMemberBinding="{Binding TagName}"
SelectedItem="{Binding SelectedVersion}"/>
<ui:FAComboBox
IsVisible="{Binding !IsReleaseMode}"
ItemsSource="{Binding AvailableCommits}"
DisplayMemberBinding="{Binding Sha}"
SelectedItem="{Binding SelectedCommit}"/>
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</StackPanel>
</controls:UserControlBase>

17
StabilityMatrix.Avalonia/Views/Dialogs/PackageImportDialog.axaml.cs

@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
public partial class PackageImportDialog : UserControlBase
{
public PackageImportDialog()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

55
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml

@ -4,11 +4,11 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
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"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="viewModels:PackageManagerViewModel"
x:CompileBindings="True"
@ -24,7 +24,7 @@
<!-- Cards -->
<ScrollViewer Grid.Row="1">
<ItemsRepeater
ItemsSource="{Binding Packages}">
ItemsSource="{Binding PackageCards}">
<ItemsRepeater.Layout>
<UniformGridLayout MinColumnSpacing="12" MinRowSpacing="12" />
</ItemsRepeater.Layout>
@ -36,7 +36,7 @@
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="{Binding Package.DisplayName}"
Text="{Binding Package.DisplayName, FallbackValue=''}"
TextAlignment="Left" />
<Button
Grid.Row="0"
@ -51,6 +51,7 @@
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuItem Header="Check for Update"
IsVisible="{Binding !IsUnknownPackage}"
Command="{Binding OnLoadedAsync}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Refresh" />
@ -79,6 +80,7 @@
VerticalAlignment="Center"
Text="{Binding InstalledVersion}" />
<!-- Normal packages shows image -->
<controls:BetterAdvancedImage
Grid.Row="2"
Grid.Column="0"
@ -89,12 +91,34 @@
HorizontalAlignment="Center"
VerticalContentAlignment="Top"
HorizontalContentAlignment="Center"
Source="{Binding CardImage}"
Source="{Binding CardImageSource}"
IsVisible="{Binding !IsUnknownPackage}"
Stretch="UniformToFill" />
<!-- Unknown packages panel -->
<Border
Grid.Row="2"
Grid.Column="0"
Margin="0,8,0,8"
Height="160"
Width="200"
CornerRadius="4"
HorizontalAlignment="Center"
IsVisible="{Binding IsUnknownPackage}"
Background="#202020">
<TextBlock
TextAlignment="Center"
VerticalAlignment="Center"
FontSize="18"
TextWrapping="Wrap"
Text="{x:Static lang:Resources.Label_UnknownPackage}"/>
</Border>
<Grid Grid.Row="3" Grid.Column="0"
IsVisible="{Binding IsUpdateAvailable}"
ColumnDefinitions="*, *">
<!-- Launch and update buttons -->
<Button Grid.Column="0" Classes="accent"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
@ -122,14 +146,33 @@
<!-- Big launch button -->
<Button Grid.Row="3" Grid.Column="0" Classes="accent"
VerticalAlignment="Bottom"
IsVisible="{Binding !IsUpdateAvailable}"
Command="{Binding Launch}"
HorizontalAlignment="Stretch">
<Button.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="!IsUpdateAvailable" />
<Binding Path="!IsUnknownPackage" />
</MultiBinding>
</Button.IsVisible>
<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" />
<TextBlock Text="{x:Static lang:Resources.Action_Launch}" />
</StackPanel>
</Button>
<!-- Import button (for unknown) -->
<Button Grid.Row="3" Grid.Column="0" Classes="transparent-info"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Command="{Binding Import}"
IsVisible="{Binding IsUnknownPackage}">
<StackPanel Orientation="Horizontal" Margin="0,2,0,2">
<faicon:Icon Value="fa-solid fa-circle-question"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="{x:Static lang:Resources.Action_Import}" />
</StackPanel>
</Button>

24
StabilityMatrix.Core/Helper/PropertyComparer.cs

@ -0,0 +1,24 @@
namespace StabilityMatrix.Core.Helper;
public class PropertyComparer<T> : IEqualityComparer<T> where T : class
{
private Func<T, object> Expr { get; set; }
public PropertyComparer(Func<T, object> expr)
{
Expr = expr;
}
public bool Equals(T? x, T? y)
{
if (x == null || y == null) return false;
var first = Expr.Invoke(x);
var second = Expr.Invoke(y);
return first.Equals(second);
}
public int GetHashCode(T obj)
{
return obj.GetHashCode();
}
}

3
StabilityMatrix.Core/Models/InstalledPackage.cs

@ -158,6 +158,9 @@ public class InstalledPackage
LibraryPath = System.IO.Path.Combine("Packages", packageFolderName);
}
public static IEqualityComparer<InstalledPackage> Comparer { get; } =
new PropertyComparer<InstalledPackage>(p => p.Id);
protected bool Equals(InstalledPackage other)
{
return Id.Equals(other.Id);

4
StabilityMatrix.Core/Models/Packages/BasePackage.cs

@ -73,8 +73,8 @@ public abstract class BasePackage
public abstract Task<IEnumerable<Branch>> GetAllBranches();
public abstract Task<IEnumerable<Release>> GetAllReleases();
public abstract string DownloadLocation { get; }
public abstract string InstallLocation { get; set; }
public virtual string? DownloadLocation { get; }
public virtual string? InstallLocation { get; set; }
public event EventHandler<ProcessOutput>? ConsoleOutput;
public event EventHandler<int>? Exited;

105
StabilityMatrix.Core/Models/Packages/UnknownPackage.cs

@ -0,0 +1,105 @@
using Octokit;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Core.Models.Packages;
public class UnknownPackage : BasePackage
{
public static string Key => "unknown-package";
public override string Name => Key;
public override string DisplayName { get; set; } = "Unknown Package";
public override string Author => "";
public override string GithubUrl => "";
public override string LicenseType => "AGPL-3.0";
public override string LicenseUrl =>
"https://github.com/LykosAI/StabilityMatrix/blob/main/LICENSE";
public override string Blurb => "A dank interface for diffusion";
public override string LaunchCommand => "test";
public override Uri PreviewImageUri => new("");
public override IReadOnlyList<string> ExtraLaunchCommands => new[]
{
"test-config",
};
/// <inheritdoc />
public override Task<string> DownloadPackage(string version, bool isCommitHash, string? branch, IProgress<ProgressReport>? progress = null)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Task InstallPackage(IProgress<ProgressReport>? progress = null)
{
throw new NotImplementedException();
}
public override Task RunPackage(string installedPackagePath, string command, string arguments)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Task SetupModelFolders(DirectoryPath installDirectory)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Task UpdateModelFolders(DirectoryPath installDirectory)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Shutdown()
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Task WaitForShutdown()
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Task<bool> CheckForUpdates(InstalledPackage package)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Task<string> Update(InstalledPackage installedPackage, IProgress<ProgressReport>? progress = null,
bool includePrerelease = false)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Task<IEnumerable<Release>> GetReleaseTags() => Task.FromResult(Enumerable.Empty<Release>());
public override List<LaunchOptionDefinition> LaunchOptions => new();
public override Task<string> GetLatestVersion() => Task.FromResult(string.Empty);
public override Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true) => Task.FromResult(Enumerable.Empty<PackageVersion>());
/// <inheritdoc />
public override Task<IEnumerable<GitCommit>?> GetAllCommits(string branch, int page = 1, int perPage = 10) => Task.FromResult<IEnumerable<GitCommit>?>(null);
/// <inheritdoc />
public override Task<IEnumerable<Branch>> GetAllBranches() => Task.FromResult(Enumerable.Empty<Branch>());
/// <inheritdoc />
public override Task<IEnumerable<Release>> GetAllReleases() => Task.FromResult(Enumerable.Empty<Release>());
}

17
StabilityMatrix.Core/Models/UnknownInstalledPackage.cs

@ -0,0 +1,17 @@
using StabilityMatrix.Core.Models.Packages;
namespace StabilityMatrix.Core.Models;
public class UnknownInstalledPackage : InstalledPackage
{
public static UnknownInstalledPackage FromDirectoryName(string name)
{
return new UnknownInstalledPackage
{
Id = Guid.NewGuid(),
PackageName = UnknownPackage.Key,
DisplayName = name,
LibraryPath = $"Packages{System.IO.Path.DirectorySeparatorChar}{name}",
};
}
}

4
StabilityMatrix.Tests/Avalonia/DesignDataTests.cs

@ -10,12 +10,12 @@ public class DesignDataTests
public static void ClassInitialize(TestContext context)
{
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
StabilityMatrix.Avalonia.DesignData.DesignData.Initialize();
DesignData.Initialize();
}
// Return all properties
public static IEnumerable<object[]> DesignDataProperties =>
typeof(StabilityMatrix.Avalonia.DesignData.DesignData).GetProperties()
typeof(DesignData).GetProperties()
.Select(p => new object[] { p });
[TestMethod]

Loading…
Cancel
Save