Browse Source

Merge branch 'main' of https://github.com/ionite34/StabilityMatrix into add-rocm-to-comfy

pull/109/head
JT 1 year ago
parent
commit
0a6a07cc8e
  1. 7
      CHANGELOG.md
  2. 18
      README.md
  3. 21
      StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs
  4. 9
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  5. 3
      StabilityMatrix.Avalonia/Languages/Resources.resx
  6. 3
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  7. 45
      StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs
  8. 11
      StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs
  9. 4
      StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
  10. 54
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  11. 8
      StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs
  12. 8
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  13. 3
      StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml
  14. 3
      StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml
  15. 91
      StabilityMatrix.Core/Database/LiteDbContext.cs
  16. 9
      StabilityMatrix.Core/Models/LaunchOptionCard.cs
  17. 14
      StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs
  18. 4
      StabilityMatrix.Core/Models/Packages/VladAutomatic.cs
  19. 86
      StabilityMatrix.Core/Updater/UpdateHelper.cs

7
CHANGELOG.md

@ -5,6 +5,11 @@ 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.3.1
### Fixed
- Fixed Auto update not appearing in some regions due to date formatting issues
- Local package import now migrates venvs and existing models
## v2.3.0 ## v2.3.0
### Added ### Added
- New installable Package - [Fooocus](https://github.com/lllyasviel/Fooocus) - New installable Package - [Fooocus](https://github.com/lllyasviel/Fooocus)
@ -19,6 +24,8 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
- Fixed issue where Checkpoints page may not show all checkpoints after clearing search filter - Fixed issue where Checkpoints page may not show all checkpoints after clearing search filter
- Fixed issue where Checkpoints page may show incorrect checkpoints for the given filter after changing pages - Fixed issue where Checkpoints page may show incorrect checkpoints for the given filter after changing pages
- Fixed issue where Open Web UI button would try to load 0.0.0.0 addresses - Fixed issue where Open Web UI button would try to load 0.0.0.0 addresses
- Fixed Dictionary error when launch arguments saved with duplicate arguments
- Fixed Launch arguments search not working
### Changed ### Changed
- Changed update method for SD.Next to use the built-in upgrade functionality - Changed update method for SD.Next to use the built-in upgrade functionality
- Model Browser navigation buttons are no longer disabled while changing pages - Model Browser navigation buttons are no longer disabled while changing pages

18
README.md

@ -13,23 +13,27 @@
[sdnext]: https://github.com/vladmandic/automatic [sdnext]: https://github.com/vladmandic/automatic
[voltaml]: https://github.com/VoltaML/voltaML-fast-stable-diffusion [voltaml]: https://github.com/VoltaML/voltaML-fast-stable-diffusion
[invokeai]: https://github.com/invoke-ai/InvokeAI [invokeai]: https://github.com/invoke-ai/InvokeAI
[fooocus]: https://github.com/lllyasviel/Fooocus
[civitai]: https://civitai.com/ [civitai]: https://civitai.com/
Multi-Platform Package Manager for Stable Diffusion Multi-Platform Package Manager for Stable Diffusion
### 🖱 One click install and update for Stable Diffusion Web UI Packages ### 🖱 One click install and update for Stable Diffusion Web UI Packages
- Supports [Automatic 1111][auto1111], [Comfy UI][comfy], [SD.Next (Vladmandic)][sdnext], [VoltaML][voltaml], [InvokeAI][invokeai] - Supports [Automatic 1111][auto1111], [Comfy UI][comfy], [SD.Next (Vladmandic)][sdnext], [VoltaML][voltaml], [InvokeAI][invokeai], and [Fooocus][fooocus]
- Embedded Git and Python dependencies, with no need for either to be globally installed - Embedded Git and Python dependencies, with no need for either to be globally installed
- Fully Portable, move Stability Matrix's Data Directory to a new drive or computer at any time - Fully portable - move Stability Matrix's Data Directory to a new drive or computer at any time
### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts ### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts
- Launch arguments editor with predefined or custom options for each Package install - Launch arguments editor with predefined or custom options for each Package install
- Package environment variables - Configurable Environment Variables
### 🗃 Checkpoint Manager, configured to be shared by all Package installs ### 🗃 Checkpoint Manager, configured to be shared by all Package installs
- Option to find CivitAI metadata and preview thumbnails for new local imports - Option to find CivitAI metadata and preview thumbnails for new local imports
### ☁ Model Browser to import from [CivitAI][civitai] ### ☁ Model Browser to import from [CivitAI][civitai]
- Automatically imports to the associated model folder depending on the model type - Automatically imports to the associated model folder depending on the model type
- Also downloads relavent metadata files and preview image - Downloads relevant metadata files and preview image
![header](https://github.com/LykosAI/StabilityMatrix/assets/13956642/a9c5f925-8561-49ba-855b-1b7bf57d7c0d) ![header](https://github.com/LykosAI/StabilityMatrix/assets/13956642/a9c5f925-8561-49ba-855b-1b7bf57d7c0d)
@ -48,17 +52,17 @@ Multi-Platform Package Manager for Stable Diffusion
### Model browser powered by [Civit AI][civitai] ### Model browser powered by [Civit AI][civitai]
- Downloads new models, automatically uses the appropriate shared model directory - Downloads new models, automatically uses the appropriate shared model directory
- Available immediately to all installed packages - Pause and resume downloads, even after closing the app
<p align="center"> <p align="center">
<img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/30b9f610-6033-4307-8d92-7d72b93cd73e" alt=""/> <img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/30b9f610-6033-4307-8d92-7d72b93cd73e" alt=""/>
</p> </p>
### Shared model directory for all your packages ### Shared model directory for all your packages
- Import local models by simple drag and drop - Import local models by simple drag and drop
- Option to find CivitAI metadata and preview thumbnails for new local imports
- Toggle visibility of categories like LoRA, VAE, CLIP, etc. - Toggle visibility of categories like LoRA, VAE, CLIP, etc.
- For models imported from Civit AI, shows additional information like version, fp precision, and preview thumbnail on hover
<p align="center"> <p align="center">
<img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/d42d1c53-67a4-45a0-b009-21400d44e17e" alt=""/> <img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/d42d1c53-67a4-45a0-b009-21400d44e17e" alt=""/>
</p> </p>

21
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

@ -133,6 +133,15 @@ public class BetterContentDialog : ContentDialog
set => SetValue(MaxDialogHeightProperty, value); set => SetValue(MaxDialogHeightProperty, value);
} }
public static readonly StyledProperty<Thickness> ContentMarginProperty = AvaloniaProperty.Register<BetterContentDialog, Thickness>(
"ContentMargin");
public Thickness ContentMargin
{
get => GetValue(ContentMarginProperty);
set => SetValue(ContentMarginProperty, value);
}
public BetterContentDialog() public BetterContentDialog()
{ {
@ -205,6 +214,18 @@ public class BetterContentDialog : ContentDialog
TryBindButtons(); TryBindButtons();
} }
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var background = e.NameScope.Find<FABorder>("BackgroundElement");
if (background is not null)
{
background.Margin = ContentMargin;
}
}
private void OnLoaded(object? sender, RoutedEventArgs? e) private void OnLoaded(object? sender, RoutedEventArgs? e)
{ {
TryBindButtons(); TryBindButtons();

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

@ -131,6 +131,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Drag &amp; Drop checkpoints here to import.
/// </summary>
public static string Label_DragAndDropCheckpointsHereToImport {
get {
return ResourceManager.GetString("Label_DragAndDropCheckpointsHereToImport", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Language. /// Looks up a localized string similar to Language.
/// </summary> /// </summary>

3
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -66,4 +66,7 @@
<data name="Label_Branches" xml:space="preserve"> <data name="Label_Branches" xml:space="preserve">
<value>Branches</value> <value>Branches</value>
</data> </data>
<data name="Label_DragAndDropCheckpointsHereToImport" xml:space="preserve">
<value>Drag &amp; Drop checkpoints here to import</value>
</data>
</root> </root>

3
StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -75,7 +74,7 @@ public partial class CheckpointFolder : ViewModelBase
? $"{Title} ({CheckpointFiles.Count + SubFolders.Sum(folder => folder.CheckpointFiles.Count)})" ? $"{Title} ({CheckpointFiles.Count + SubFolders.Sum(folder => folder.CheckpointFiles.Count)})"
: Title; : Title;
public Base.ProgressViewModel Progress { get; } = new(); public ProgressViewModel Progress { get; } = new();
public CheckpointFolder? ParentFolder { get; init; } public CheckpointFolder? ParentFolder { get; init; }
public AdvancedObservableList<CheckpointFolder> SubFolders { get; init; } = new(); public AdvancedObservableList<CheckpointFolder> SubFolders { get; init; } = new();

45
StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs

@ -1,9 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using FuzzySharp; using FuzzySharp;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Cache;
@ -14,13 +18,16 @@ namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(LaunchOptionsDialog))] [View(typeof(LaunchOptionsDialog))]
public partial class LaunchOptionsViewModel : ContentDialogViewModelBase public partial class LaunchOptionsViewModel : ContentDialogViewModelBase
{ {
private readonly ILogger<LaunchOptionsViewModel> logger;
private readonly LRUCache<string, ImmutableList<LaunchOptionCard>> cache = new(100); private readonly LRUCache<string, ImmutableList<LaunchOptionCard>> cache = new(100);
[ObservableProperty] private string title = "Launch Options"; [ObservableProperty]
[ObservableProperty] private bool isSearchBoxEnabled = true; private string title = "Launch Options";
[ObservableProperty]
private bool isSearchBoxEnabled = true;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(FilteredCards))]
private string searchText = string.Empty; private string searchText = string.Empty;
[ObservableProperty] [ObservableProperty]
@ -31,9 +38,8 @@ public partial class LaunchOptionsViewModel : ContentDialogViewModelBase
/// <summary> /// <summary>
/// Return cards that match the search text /// Return cards that match the search text
/// </summary> /// </summary>
private IReadOnlyList<LaunchOptionCard>? GetFilteredCards() private IReadOnlyList<LaunchOptionCard>? GetFilteredCards(string? text)
{ {
var text = SearchText;
if (string.IsNullOrWhiteSpace(text) || text.Length < 2) if (string.IsNullOrWhiteSpace(text) || text.Length < 2)
{ {
return Cards; return Cards;
@ -51,17 +57,29 @@ public partial class LaunchOptionsViewModel : ContentDialogViewModelBase
Options = Array.Empty<LaunchOption>() Options = Array.Empty<LaunchOption>()
}; };
var extracted = Process var extracted = Process.ExtractTop(searchCard, Cards, c => c.Title.ToLowerInvariant());
.ExtractTop(searchCard, Cards, c => c.Title.ToLowerInvariant()); var results = extracted.Where(r => r.Score > 40).Select(r => r.Value).ToImmutableList();
var results = extracted
.Where(r => r.Score > 40)
.Select(r => r.Value)
.ToImmutableList();
cache.Add(text, results); cache.Add(text, results);
return results; return results;
} }
public void UpdateFilterCards() => FilteredCards = GetFilteredCards(); public void UpdateFilterCards() => FilteredCards = GetFilteredCards(SearchText);
public LaunchOptionsViewModel(ILogger<LaunchOptionsViewModel> logger)
{
this.logger = logger;
Observable
.FromEventPattern<PropertyChangedEventArgs>(this, nameof(PropertyChanged))
.Where(x => x.EventArgs.PropertyName == nameof(SearchText))
.Throttle(TimeSpan.FromMilliseconds(50))
.Select(_ => SearchText)
.ObserveOn(SynchronizationContext.Current!)
.Subscribe(
text => FilteredCards = GetFilteredCards(text),
err => logger.LogError(err, "Error while filtering launch options")
);
}
public override void OnLoaded() public override void OnLoaded()
{ {
@ -75,7 +93,8 @@ public partial class LaunchOptionsViewModel : ContentDialogViewModelBase
public List<LaunchOption> AsLaunchArgs() public List<LaunchOption> AsLaunchArgs()
{ {
var launchArgs = new List<LaunchOption>(); var launchArgs = new List<LaunchOption>();
if (Cards is null) return launchArgs; if (Cards is null)
return launchArgs;
foreach (var card in Cards) foreach (var card in Cards)
{ {

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

@ -183,7 +183,7 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
} }
} }
public void AddPackageWithCurrentInputs() public async Task AddPackageWithCurrentInputs()
{ {
if (SelectedBasePackage is null || PackagePath is null) if (SelectedBasePackage is null || PackagePath is null)
return; return;
@ -216,6 +216,15 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
LastUpdateCheck = DateTimeOffset.Now, LastUpdateCheck = DateTimeOffset.Now,
}; };
// Recreate venv if it's a BaseGitPackage
if (SelectedBasePackage is BaseGitPackage gitPackage)
{
await gitPackage.SetupVenv(PackagePath, forceRecreate: true);
}
// Reconfigure shared links
await SelectedBasePackage.UpdateModelFolders(PackagePath);
settingsManager.Transaction(s => s.InstalledPackages.Add(package)); settingsManager.Transaction(s => s.InstalledPackages.Add(package));
} }
} }

4
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

@ -33,7 +33,6 @@ using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Notification = Avalonia.Controls.Notifications.Notification;
using Symbol = FluentIcons.Common.Symbol; using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -305,13 +304,16 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
IsPrimaryButtonEnabled = true, IsPrimaryButtonEnabled = true,
PrimaryButtonText = "Save", PrimaryButtonText = "Save",
CloseButtonText = "Cancel", CloseButtonText = "Cancel",
FullSizeDesired = true,
DefaultButton = ContentDialogButton.Primary, DefaultButton = ContentDialogButton.Primary,
ContentMargin = new Thickness(32, 16),
Padding = new Thickness(0, 16), Padding = new Thickness(0, 16),
Content = new LaunchOptionsDialog Content = new LaunchOptionsDialog
{ {
DataContext = viewModel, DataContext = viewModel,
} }
}; };
var result = await dialog.ShowAsync(); var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary) if (result == ContentDialogResult.Primary)

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

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
@ -11,6 +12,7 @@ using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
@ -219,25 +221,57 @@ public partial class PackageCardViewModel : ProgressViewModel
{ {
if (!IsUnknownPackage || Design.IsDesignMode) return; if (!IsUnknownPackage || Design.IsDesignMode) return;
PackageImportViewModel viewModel = null!; var viewModel = vmFactory.Get<PackageImportViewModel>(vm =>
var dialog = vmFactory.GetDialog<PackageImportViewModel>(vm =>
{ {
vm.PackagePath = new DirectoryPath(Package?.FullPath ?? throw new InvalidOperationException()); vm.PackagePath =
viewModel = vm; new DirectoryPath(Package?.FullPath ?? throw new InvalidOperationException());
}); });
dialog.PrimaryButtonText = Resources.Action_Import; var dialog = new TaskDialog
dialog.CloseButtonText = Resources.Action_Cancel; {
dialog.DefaultButton = ContentDialogButton.Primary; Content = new PackageImportDialog
{
DataContext = viewModel
},
ShowProgressBar = false,
Buttons = new List<TaskDialogButton>
{
new(Resources.Action_Import, TaskDialogStandardResult.Yes)
{
IsDefault = true
},
new(Resources.Action_Cancel, TaskDialogStandardResult.Cancel)
}
};
var result = await dialog.ShowAsync(); dialog.Closing += async (sender, e) =>
if (result == ContentDialogResult.Primary) {
// We only want to use the deferral on the 'Yes' Button
if ((TaskDialogStandardResult)e.Result == TaskDialogStandardResult.Yes)
{
var deferral = e.GetDeferral();
sender.ShowProgressBar = true;
sender.SetProgressBarState(0, TaskDialogProgressState.Indeterminate);
await using (new MinimumDelay(200, 300))
{
var result = await notificationService.TryAsync(viewModel.AddPackageWithCurrentInputs());
if (result.IsSuccessful)
{ {
viewModel.AddPackageWithCurrentInputs();
EventManager.Instance.OnInstalledPackagesChanged(); EventManager.Instance.OnInstalledPackagesChanged();
} }
} }
deferral.Complete();
}
};
dialog.XamlRoot = App.VisualRoot;
await dialog.ShowAsync(true);
}
public async Task OpenFolder() public async Task OpenFolder()
{ {
if (string.IsNullOrWhiteSpace(Package?.FullPath)) if (string.IsNullOrWhiteSpace(Package?.FullPath))

8
StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs

@ -6,12 +6,8 @@ using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels;
public partial class ProgressItemViewModel : ProgressItemViewModelBase public class ProgressItemViewModel : ProgressItemViewModelBase
{ {
[ObservableProperty] private Guid id;
[ObservableProperty] private string name;
[ObservableProperty] private bool failed;
public ProgressItemViewModel(ProgressItem progressItem) public ProgressItemViewModel(ProgressItem progressItem)
{ {
Id = progressItem.ProgressId; Id = progressItem.ProgressId;
@ -19,6 +15,7 @@ public partial class ProgressItemViewModel : ProgressItemViewModelBase
Progress.Value = progressItem.Progress.Percentage; Progress.Value = progressItem.Progress.Percentage;
Failed = progressItem.Failed; Failed = progressItem.Failed;
Progress.Text = GetProgressText(progressItem.Progress); Progress.Text = GetProgressText(progressItem.Progress);
Progress.IsIndeterminate = progressItem.Progress.IsIndeterminate;
EventManager.Instance.ProgressChanged += OnProgressChanged; EventManager.Instance.ProgressChanged += OnProgressChanged;
} }
@ -31,6 +28,7 @@ public partial class ProgressItemViewModel : ProgressItemViewModelBase
Progress.Value = e.Progress.Percentage; Progress.Value = e.Progress.Percentage;
Failed = e.Failed; Failed = e.Failed;
Progress.Text = GetProgressText(e.Progress); Progress.Text = GetProgressText(e.Progress);
Progress.IsIndeterminate = e.Progress.IsIndeterminate;
} }
private string GetProgressText(ProgressReport report) private string GetProgressText(ProgressReport report)

8
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -8,6 +8,7 @@
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:checkpointManager="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager" xmlns:checkpointManager="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static mocks:DesignData.CheckpointsPageViewModel}" d:DataContext="{x:Static mocks:DesignData.CheckpointsPageViewModel}"
x:CompileBindings="True" x:CompileBindings="True"
x:DataType="vm:CheckpointsPageViewModel" x:DataType="vm:CheckpointsPageViewModel"
@ -255,7 +256,6 @@
Background="Transparent" Background="Transparent"
DragDrop.AllowDrop="True"> DragDrop.AllowDrop="True">
<ItemsRepeater <ItemsRepeater
Classes="ItemsDragAndDrop"
ItemTemplate="{StaticResource CheckpointFileDataTemplate}" ItemTemplate="{StaticResource CheckpointFileDataTemplate}"
ItemsSource="{Binding DisplayedCheckpointFiles}"> ItemsSource="{Binding DisplayedCheckpointFiles}">
<ItemsRepeater.Layout> <ItemsRepeater.Layout>
@ -266,8 +266,8 @@
<TextBlock VerticalAlignment="Center" <TextBlock VerticalAlignment="Center"
TextAlignment="Center" TextAlignment="Center"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Margin="8" Margin="8,8,8,16"
Text="Drag &amp; drop checkpoints here to import" Text="{x:Static lang:Resources.Label_DragAndDropCheckpointsHereToImport}"
IsVisible="{Binding !CheckpointFiles.Count}"/> IsVisible="{Binding !CheckpointFiles.Count}"/>
<!-- Blurred background for drag and drop --> <!-- Blurred background for drag and drop -->
<Border <Border
@ -284,7 +284,7 @@
Effect="{StaticResource TextDropShadowEffect}" Effect="{StaticResource TextDropShadowEffect}"
FontSize="24" FontSize="24"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Text="Drop a file here to import" Text="Drop file here to import"
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding IsCurrentDragTarget}" /> IsVisible="{Binding IsCurrentDragTarget}" />
<StackPanel <StackPanel

3
StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml

@ -135,7 +135,8 @@
TextWrapping="Wrap" TextWrapping="Wrap"
IsVisible="{Binding SelectedPackage.Disclaimer, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> IsVisible="{Binding SelectedPackage.Disclaimer, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0"> <StackPanel Orientation="Horizontal" Margin="0,8,0,0"
IsVisible="{Binding IsReleaseModeAvailable}">
<ToggleButton <ToggleButton
Content="Releases" Content="Releases"
IsChecked="{Binding IsReleaseMode}" IsChecked="{Binding IsReleaseMode}"

3
StabilityMatrix.Avalonia/Views/Dialogs/LaunchOptionsDialog.axaml

@ -23,7 +23,7 @@
</controls:UserControlBase.Resources> </controls:UserControlBase.Resources>
<Grid MinWidth="400" RowDefinitions="0.2*,0.8*" Margin="8"> <Grid MinWidth="400" RowDefinitions="Auto,*" Margin="8">
<StackPanel <StackPanel
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Spacing="4" Spacing="4"
@ -54,6 +54,7 @@
<!-- Option Cards --> <!-- Option Cards -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"> <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<ItemsControl <ItemsControl
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Padding="8" Padding="8"
ItemsSource="{Binding FilteredCards}"> ItemsSource="{Binding FilteredCards}">

91
StabilityMatrix.Core/Database/LiteDbContext.cs

@ -1,4 +1,6 @@
using LiteDB.Async; using LiteDB;
using LiteDB.Async;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api;
@ -10,27 +12,12 @@ namespace StabilityMatrix.Core.Database;
public class LiteDbContext : ILiteDbContext public class LiteDbContext : ILiteDbContext
{ {
private readonly ILogger<LiteDbContext> logger;
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly DebugOptions debugOptions; private readonly DebugOptions debugOptions;
private LiteDatabaseAsync? database; private LiteDatabaseAsync? database;
public LiteDatabaseAsync Database public LiteDatabaseAsync Database => database ??= CreateDatabase();
{
get
{
if (database != null) return database;
var connectionString = debugOptions.TempDatabase ? ":temp:"
: $"Filename={Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db")};Mode=Shared";
database = new LiteDatabaseAsync(connectionString);
// Register reference fields
LiteDBExtensions.Register<CivitModel, CivitModelVersion>(m => m.ModelVersions, "CivitModelVersions");
LiteDBExtensions.Register<CivitModelQueryCacheEntry, CivitModel>(e => e.Items, "CivitModels");
return database;
}
}
// Notification events // Notification events
public event EventHandler? CivitModelsChanged; public event EventHandler? CivitModelsChanged;
@ -41,12 +28,50 @@ public class LiteDbContext : ILiteDbContext
public ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache => Database.GetCollection<CivitModelQueryCacheEntry>("CivitModelQueryCache"); public ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache => Database.GetCollection<CivitModelQueryCacheEntry>("CivitModelQueryCache");
public ILiteCollectionAsync<GithubCacheEntry> GithubCache => Database.GetCollection<GithubCacheEntry>("GithubCache"); public ILiteCollectionAsync<GithubCacheEntry> GithubCache => Database.GetCollection<GithubCacheEntry>("GithubCache");
public LiteDbContext(ISettingsManager settingsManager, IOptions<DebugOptions> debugOptions) public LiteDbContext(
ILogger<LiteDbContext> logger,
ISettingsManager settingsManager,
IOptions<DebugOptions> debugOptions)
{ {
this.logger = logger;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.debugOptions = debugOptions.Value; this.debugOptions = debugOptions.Value;
} }
private LiteDatabaseAsync CreateDatabase()
{
LiteDatabaseAsync? db = null;
if (debugOptions.TempDatabase)
{
db = new LiteDatabaseAsync(":temp:");
}
// Attempt to create connection, might be in use
try
{
var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db");
db = new LiteDatabaseAsync(new ConnectionString()
{
Filename = dbPath,
Connection = ConnectionType.Shared,
});
}
catch (IOException e)
{
logger.LogWarning("Database in use or not accessible ({Message}), using temporary database", e.Message);
}
// Fallback to temporary database
db ??= new LiteDatabaseAsync(":temp:");
// Register reference fields
LiteDBExtensions.Register<CivitModel, CivitModelVersion>(m => m.ModelVersions, "CivitModelVersions");
LiteDBExtensions.Register<CivitModelQueryCacheEntry, CivitModel>(e => e.Items, "CivitModels");
return db;
}
public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3) public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3)
{ {
var version = await CivitModelVersions.Query() var version = await CivitModelVersions.Query()
@ -54,22 +79,26 @@ public class LiteDbContext : ILiteDbContext
.Select(f => f.Hashes) .Select(f => f.Hashes)
.Select(hashes => hashes.BLAKE3) .Select(hashes => hashes.BLAKE3)
.Any(hash => hash == hashBlake3)) .Any(hash => hash == hashBlake3))
.FirstOrDefaultAsync(); .FirstOrDefaultAsync()
.ConfigureAwait(false);
if (version is null) return (null, null); if (version is null) return (null, null);
var model = await CivitModels.Query() var model = await CivitModels.Query()
.Include(m => m.ModelVersions) .Include(m => m.ModelVersions)
.Where(m => m.ModelVersions! .Where(m => m.ModelVersions!
.Select(v => v.Id) .Select(v => v.Id)
.Any(id => id == version.Id)) .Any(id => id == version.Id))
.FirstOrDefaultAsync(); .FirstOrDefaultAsync().ConfigureAwait(false);
return (model, version); return (model, version);
} }
public async Task<bool> UpsertCivitModelAsync(CivitModel civitModel) public async Task<bool> UpsertCivitModelAsync(CivitModel civitModel)
{ {
// Insert model versions first then model // Insert model versions first then model
var versionsUpdated = await CivitModelVersions.UpsertAsync(civitModel.ModelVersions); var versionsUpdated = await CivitModelVersions.UpsertAsync(civitModel.ModelVersions).ConfigureAwait(false);
var updated = await CivitModels.UpsertAsync(civitModel); var updated = await CivitModels.UpsertAsync(civitModel).ConfigureAwait(false);
// Notify listeners on any change // Notify listeners on any change
var anyUpdated = versionsUpdated > 0 || updated; var anyUpdated = versionsUpdated > 0 || updated;
if (anyUpdated) if (anyUpdated)
@ -84,8 +113,8 @@ public class LiteDbContext : ILiteDbContext
var civitModelsArray = civitModels.ToArray(); var civitModelsArray = civitModels.ToArray();
// Get all model versions then insert models // Get all model versions then insert models
var versions = civitModelsArray.SelectMany(model => model.ModelVersions ?? new()); var versions = civitModelsArray.SelectMany(model => model.ModelVersions ?? new());
var versionsUpdated = await CivitModelVersions.UpsertAsync(versions); var versionsUpdated = await CivitModelVersions.UpsertAsync(versions).ConfigureAwait(false);
var updated = await CivitModels.UpsertAsync(civitModelsArray); var updated = await CivitModels.UpsertAsync(civitModelsArray).ConfigureAwait(false);
// Notify listeners on any change // Notify listeners on any change
var anyUpdated = versionsUpdated > 0 || updated > 0; var anyUpdated = versionsUpdated > 0 || updated > 0;
if (updated > 0 || versionsUpdated > 0) if (updated > 0 || versionsUpdated > 0)
@ -98,7 +127,7 @@ public class LiteDbContext : ILiteDbContext
// Add to cache // Add to cache
public async Task<bool> UpsertCivitModelQueryCacheEntryAsync(CivitModelQueryCacheEntry entry) public async Task<bool> UpsertCivitModelQueryCacheEntryAsync(CivitModelQueryCacheEntry entry)
{ {
var changed = await CivitModelQueryCache.UpsertAsync(entry); var changed = await CivitModelQueryCache.UpsertAsync(entry).ConfigureAwait(false);
if (changed) if (changed)
{ {
CivitModelsChanged?.Invoke(this, EventArgs.Empty); CivitModelsChanged?.Invoke(this, EventArgs.Empty);
@ -107,8 +136,14 @@ public class LiteDbContext : ILiteDbContext
return changed; return changed;
} }
public Task<GithubCacheEntry?> GetGithubCacheEntry(string cacheKey) => public async Task<GithubCacheEntry?> GetGithubCacheEntry(string cacheKey)
GithubCache.FindByIdAsync(cacheKey); {
if (await GithubCache.FindByIdAsync(cacheKey).ConfigureAwait(false) is { } result)
{
return result;
}
return null;
}
public Task<bool> UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) => public Task<bool> UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) =>
GithubCache.UpsertAsync(cacheEntry); GithubCache.UpsertAsync(cacheEntry);

9
StabilityMatrix.Core/Models/LaunchOptionCard.cs

@ -45,8 +45,13 @@ public readonly record struct LaunchOptionCard
// During card creation, store dict of options with initial values // During card creation, store dict of options with initial values
var initialOptions = new Dictionary<string, object>(); var initialOptions = new Dictionary<string, object>();
// Dict of // To dictionary ignoring duplicates
var launchArgsDict = launchArgs.ToDictionary(launchArg => launchArg.Name); var launchArgsDict = launchArgs
.ToLookup(launchArg => launchArg.Name)
.ToDictionary(
group => group.Key,
group => group.First()
);
// Create cards // Create cards
foreach (var definition in definitions) foreach (var definition in definitions)

14
StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs

@ -105,13 +105,13 @@ public abstract class BaseGitPackage : BasePackage
/// <summary> /// <summary>
/// Setup the virtual environment for the package. /// Setup the virtual environment for the package.
/// </summary> /// </summary>
/// <param name="installedPackagePath"></param>
/// <param name="venvName"></param>
/// <returns></returns>
[MemberNotNull(nameof(VenvRunner))] [MemberNotNull(nameof(VenvRunner))]
protected async Task<PyVenvRunner> SetupVenv(string installedPackagePath, string venvName = "venv") public async Task<PyVenvRunner> SetupVenv(
string installedPackagePath,
string venvName = "venv",
bool forceRecreate = false)
{ {
var venvPath = Path.Combine(installedPackagePath, "venv"); var venvPath = Path.Combine(installedPackagePath, venvName);
if (VenvRunner != null) if (VenvRunner != null)
{ {
await VenvRunner.DisposeAsync().ConfigureAwait(false); await VenvRunner.DisposeAsync().ConfigureAwait(false);
@ -123,9 +123,9 @@ public abstract class BaseGitPackage : BasePackage
EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables, EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables,
}; };
if (!VenvRunner.Exists()) if (!VenvRunner.Exists() || forceRecreate)
{ {
await VenvRunner.Setup().ConfigureAwait(false); await VenvRunner.Setup(forceRecreate).ConfigureAwait(false);
} }
return VenvRunner; return VenvRunner;
} }

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

@ -191,7 +191,7 @@ public class VladAutomatic : BaseGitPackage
public override async Task<string> DownloadPackage(string version, bool isCommitHash, public override async Task<string> DownloadPackage(string version, bool isCommitHash,
string? branch, IProgress<ProgressReport>? progress = null) string? branch, IProgress<ProgressReport>? progress = null)
{ {
progress?.Report(new ProgressReport(0.1f, message: "Downloading package...", progress?.Report(new ProgressReport(-1f, message: "Downloading package...",
isIndeterminate: true, type: ProgressType.Download)); isIndeterminate: true, type: ProgressType.Download));
var installDir = new DirectoryPath(InstallLocation); var installDir = new DirectoryPath(InstallLocation);
@ -245,7 +245,7 @@ public class VladAutomatic : BaseGitPackage
throw new Exception("Installed branch is null"); throw new Exception("Installed branch is null");
} }
progress?.Report(new ProgressReport(0.1f, message: "Downloading package update...", progress?.Report(new ProgressReport(-1f, message: "Downloading package update...",
isIndeterminate: true, type: ProgressType.Update)); isIndeterminate: true, type: ProgressType.Update));
await PrerequisiteHelper.RunGit(installedPackage.FullPath, "checkout", await PrerequisiteHelper.RunGit(installedPackage.FullPath, "checkout",

86
StabilityMatrix.Core/Updater/UpdateHelper.cs

@ -1,4 +1,5 @@
using System.Text.Json; using System.Globalization;
using System.Text.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
@ -19,23 +20,30 @@ public class UpdateHelper : IUpdateHelper
private readonly DebugOptions debugOptions; private readonly DebugOptions debugOptions;
private readonly System.Timers.Timer timer = new(TimeSpan.FromMinutes(5)); private readonly System.Timers.Timer timer = new(TimeSpan.FromMinutes(5));
private string UpdateManifestUrl => debugOptions.UpdateManifestUrl ?? private string UpdateManifestUrl =>
"https://cdn.lykos.ai/update-v2.json"; debugOptions.UpdateManifestUrl ?? "https://cdn.lykos.ai/update-v2.json";
public const string UpdateFolderName = ".StabilityMatrixUpdate"; public const string UpdateFolderName = ".StabilityMatrixUpdate";
public static DirectoryPath UpdateFolder => Compat.AppCurrentDir.JoinDir(UpdateFolderName); public static DirectoryPath UpdateFolder => Compat.AppCurrentDir.JoinDir(UpdateFolderName);
public static FilePath ExecutablePath => UpdateFolder.JoinFile(Compat.GetExecutableName()); public static FilePath ExecutablePath => UpdateFolder.JoinFile(Compat.GetExecutableName());
public UpdateHelper(ILogger<UpdateHelper> logger, IHttpClientFactory httpClientFactory, public UpdateHelper(
IDownloadService downloadService, IOptions<DebugOptions> debugOptions) ILogger<UpdateHelper> logger,
IHttpClientFactory httpClientFactory,
IDownloadService downloadService,
IOptions<DebugOptions> debugOptions
)
{ {
this.logger = logger; this.logger = logger;
this.httpClientFactory = httpClientFactory; this.httpClientFactory = httpClientFactory;
this.downloadService = downloadService; this.downloadService = downloadService;
this.debugOptions = debugOptions.Value; this.debugOptions = debugOptions.Value;
timer.Elapsed += async (_, _) => { await CheckForUpdate().ConfigureAwait(false); }; timer.Elapsed += async (_, _) =>
{
await CheckForUpdate().ConfigureAwait(false);
};
} }
public async Task StartCheckingForUpdates() public async Task StartCheckingForUpdates()
@ -45,18 +53,33 @@ public class UpdateHelper : IUpdateHelper
await CheckForUpdate().ConfigureAwait(false); await CheckForUpdate().ConfigureAwait(false);
} }
public async Task DownloadUpdate(UpdateInfo updateInfo, public async Task DownloadUpdate(UpdateInfo updateInfo, IProgress<ProgressReport> progress)
IProgress<ProgressReport> progress)
{ {
var downloadUrl = updateInfo.DownloadUrl; var downloadUrl = updateInfo.DownloadUrl;
Directory.CreateDirectory(UpdateFolder); Directory.CreateDirectory(UpdateFolder);
// download the file from URL // download the file from URL
await downloadService.DownloadToFileAsync(downloadUrl, ExecutablePath, progress: progress, await downloadService
httpClientName: "UpdateClient").ConfigureAwait(false); .DownloadToFileAsync(
downloadUrl,
ExecutablePath,
progress: progress,
httpClientName: "UpdateClient"
)
.ConfigureAwait(false);
} }
/// <summary>
/// Format a DatetimeOffset to a culture invariant string for use in signature verification.
/// </summary>
private static string FormatDateTimeOffsetInvariant(DateTimeOffset dateTimeOffset)
{
return dateTimeOffset.ToString(
@"yyyy-MM-ddTHH\:mm\:ss.ffffffzzz",
CultureInfo.InvariantCulture
);
}
/// <summary> /// <summary>
/// Data for use in signature verification. /// Data for use in signature verification.
@ -66,10 +89,10 @@ public class UpdateHelper : IUpdateHelper
private static string GetUpdateInfoSignedData(UpdateInfo updateInfo) private static string GetUpdateInfoSignedData(UpdateInfo updateInfo)
{ {
var channel = updateInfo.Channel.GetStringValue().ToLowerInvariant(); var channel = updateInfo.Channel.GetStringValue().ToLowerInvariant();
var date = updateInfo.ReleaseDate.ToString("yyyy-MM-ddTHH:mm:ss.ffffffzzz"); var date = FormatDateTimeOffsetInvariant(updateInfo.ReleaseDate);
return $"{updateInfo.Version};{date};{channel};" + return $"{updateInfo.Version};{date};{channel};"
$"{(int) updateInfo.Type};{updateInfo.DownloadUrl};{updateInfo.ChangelogUrl};" + + $"{(int)updateInfo.Type};{updateInfo.DownloadUrl};{updateInfo.ChangelogUrl};"
$"{updateInfo.HashBlake3}"; + $"{updateInfo.HashBlake3}";
} }
private async Task CheckForUpdate() private async Task CheckForUpdate()
@ -80,15 +103,19 @@ public class UpdateHelper : IUpdateHelper
var response = await httpClient.GetAsync(UpdateManifestUrl).ConfigureAwait(false); var response = await httpClient.GetAsync(UpdateManifestUrl).ConfigureAwait(false);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
logger.LogWarning("Error while checking for update {StatusCode} - {Content}", logger.LogWarning(
response.StatusCode, await response.Content.ReadAsStringAsync().ConfigureAwait(false)); "Error while checking for update {StatusCode} - {Content}",
response.StatusCode,
await response.Content.ReadAsStringAsync().ConfigureAwait(false)
);
return; return;
} }
var updateCollection = var updateCollection = await JsonSerializer
await JsonSerializer.DeserializeAsync<UpdateCollection>( .DeserializeAsync<UpdateCollection>(
await response.Content.ReadAsStreamAsync() await response.Content.ReadAsStreamAsync().ConfigureAwait(false)
.ConfigureAwait(false)).ConfigureAwait(false); )
.ConfigureAwait(false);
if (updateCollection is null) if (updateCollection is null)
{ {
@ -106,7 +133,10 @@ public class UpdateHelper : IUpdateHelper
if (updateInfo is null) if (updateInfo is null)
{ {
logger.LogWarning("Could not find compatible update info for the platform {Platform}", Compat.Platform); logger.LogWarning(
"Could not find compatible update info for the platform {Platform}",
Compat.Platform
);
return; return;
} }
@ -129,8 +159,11 @@ public class UpdateHelper : IUpdateHelper
if (order > 0) if (order > 0)
{ {
// Newer version available // Newer version available
logger.LogInformation("Update available {AppVer} -> {UpdateVer}", logger.LogInformation(
Compat.AppVersion, updateInfo.Version); "Update available {AppVer} -> {UpdateVer}",
Compat.AppVersion,
updateInfo.Version
);
EventManager.Instance.OnUpdateAvailable(updateInfo); EventManager.Instance.OnUpdateAvailable(updateInfo);
return; return;
} }
@ -142,8 +175,11 @@ public class UpdateHelper : IUpdateHelper
// If different, we can update // If different, we can update
if (updateHash != appHash) if (updateHash != appHash)
{ {
logger.LogInformation("Update available {AppVer} -> {UpdateVer}", logger.LogInformation(
Compat.AppVersion, updateInfo.Version); "Update available {AppVer} -> {UpdateVer}",
Compat.AppVersion,
updateInfo.Version
);
EventManager.Instance.OnUpdateAvailable(updateInfo); EventManager.Instance.OnUpdateAvailable(updateInfo);
return; return;
} }

Loading…
Cancel
Save