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/),
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
### Added
- 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 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 Dictionary error when launch arguments saved with duplicate arguments
- Fixed Launch arguments search not working
### Changed
- Changed update method for SD.Next to use the built-in upgrade functionality
- Model Browser navigation buttons are no longer disabled while changing pages

18
README.md

@ -13,23 +13,27 @@
[sdnext]: https://github.com/vladmandic/automatic
[voltaml]: https://github.com/VoltaML/voltaML-fast-stable-diffusion
[invokeai]: https://github.com/invoke-ai/InvokeAI
[fooocus]: https://github.com/lllyasviel/Fooocus
[civitai]: https://civitai.com/
Multi-Platform Package Manager for Stable Diffusion
### 🖱 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
- 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
- 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
- Option to find CivitAI metadata and preview thumbnails for new local imports
### ☁ Model Browser to import from [CivitAI][civitai]
- 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)
@ -48,17 +52,17 @@ Multi-Platform Package Manager for Stable Diffusion
### Model browser powered by [Civit AI][civitai]
- 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">
<img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/30b9f610-6033-4307-8d92-7d72b93cd73e" alt=""/>
</p>
### Shared model directory for all your packages
- 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.
- For models imported from Civit AI, shows additional information like version, fp precision, and preview thumbnail on hover
<p align="center">
<img style="width: 80%; height: 80%" src="https://github.com/LykosAI/StabilityMatrix/assets/13956642/d42d1c53-67a4-45a0-b009-21400d44e17e" alt=""/>
</p>

21
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

@ -133,6 +133,15 @@ public class BetterContentDialog : ContentDialog
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()
{
@ -205,6 +214,18 @@ public class BetterContentDialog : ContentDialog
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)
{
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>
/// Looks up a localized string similar to Language.
/// </summary>

3
StabilityMatrix.Avalonia/Languages/Resources.resx

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

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

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

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

@ -1,9 +1,13 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FuzzySharp;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper.Cache;
@ -14,13 +18,16 @@ namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(LaunchOptionsDialog))]
public partial class LaunchOptionsViewModel : ContentDialogViewModelBase
{
private readonly ILogger<LaunchOptionsViewModel> logger;
private readonly LRUCache<string, ImmutableList<LaunchOptionCard>> cache = new(100);
[ObservableProperty] private string title = "Launch Options";
[ObservableProperty] private bool isSearchBoxEnabled = true;
[ObservableProperty]
private string title = "Launch Options";
[ObservableProperty]
private bool isSearchBoxEnabled = true;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FilteredCards))]
private string searchText = string.Empty;
[ObservableProperty]
@ -31,9 +38,8 @@ public partial class LaunchOptionsViewModel : ContentDialogViewModelBase
/// <summary>
/// Return cards that match the search text
/// </summary>
private IReadOnlyList<LaunchOptionCard>? GetFilteredCards()
private IReadOnlyList<LaunchOptionCard>? GetFilteredCards(string? text)
{
var text = SearchText;
if (string.IsNullOrWhiteSpace(text) || text.Length < 2)
{
return Cards;
@ -51,17 +57,29 @@ public partial class LaunchOptionsViewModel : ContentDialogViewModelBase
Options = Array.Empty<LaunchOption>()
};
var extracted = Process
.ExtractTop(searchCard, Cards, c => c.Title.ToLowerInvariant());
var results = extracted
.Where(r => r.Score > 40)
.Select(r => r.Value)
.ToImmutableList();
var extracted = Process.ExtractTop(searchCard, Cards, c => c.Title.ToLowerInvariant());
var results = extracted.Where(r => r.Score > 40).Select(r => r.Value).ToImmutableList();
cache.Add(text, 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()
{
@ -75,7 +93,8 @@ public partial class LaunchOptionsViewModel : ContentDialogViewModelBase
public List<LaunchOption> AsLaunchArgs()
{
var launchArgs = new List<LaunchOption>();
if (Cards is null) return launchArgs;
if (Cards is null)
return launchArgs;
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)
return;
@ -216,6 +216,15 @@ public partial class PackageImportViewModel : ContentDialogViewModelBase
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));
}
}

4
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

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

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

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
@ -11,6 +12,7 @@ using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
@ -219,25 +221,57 @@ public partial class PackageCardViewModel : ProgressViewModel
{
if (!IsUnknownPackage || Design.IsDesignMode) return;
PackageImportViewModel viewModel = null!;
var dialog = vmFactory.GetDialog<PackageImportViewModel>(vm =>
var viewModel = vmFactory.Get<PackageImportViewModel>(vm =>
{
vm.PackagePath = new DirectoryPath(Package?.FullPath ?? throw new InvalidOperationException());
viewModel = vm;
vm.PackagePath =
new DirectoryPath(Package?.FullPath ?? throw new InvalidOperationException());
});
dialog.PrimaryButtonText = Resources.Action_Import;
dialog.CloseButtonText = Resources.Action_Cancel;
dialog.DefaultButton = ContentDialogButton.Primary;
var dialog = new TaskDialog
{
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();
if (result == ContentDialogResult.Primary)
dialog.Closing += async (sender, e) =>
{
// 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();
}
}
deferral.Complete();
}
};
dialog.XamlRoot = App.VisualRoot;
await dialog.ShowAsync(true);
}
public async Task OpenFolder()
{
if (string.IsNullOrWhiteSpace(Package?.FullPath))

8
StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs

@ -6,12 +6,8 @@ using StabilityMatrix.Core.Models.Progress;
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)
{
Id = progressItem.ProgressId;
@ -19,6 +15,7 @@ public partial class ProgressItemViewModel : ProgressItemViewModelBase
Progress.Value = progressItem.Progress.Percentage;
Failed = progressItem.Failed;
Progress.Text = GetProgressText(progressItem.Progress);
Progress.IsIndeterminate = progressItem.Progress.IsIndeterminate;
EventManager.Instance.ProgressChanged += OnProgressChanged;
}
@ -31,6 +28,7 @@ public partial class ProgressItemViewModel : ProgressItemViewModelBase
Progress.Value = e.Progress.Percentage;
Failed = e.Failed;
Progress.Text = GetProgressText(e.Progress);
Progress.IsIndeterminate = e.Progress.IsIndeterminate;
}
private string GetProgressText(ProgressReport report)

8
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

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

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

@ -135,7 +135,8 @@
TextWrapping="Wrap"
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
Content="Releases"
IsChecked="{Binding IsReleaseMode}"

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

@ -23,7 +23,7 @@
</controls:UserControlBase.Resources>
<Grid MinWidth="400" RowDefinitions="0.2*,0.8*" Margin="8">
<Grid MinWidth="400" RowDefinitions="Auto,*" Margin="8">
<StackPanel
HorizontalAlignment="Stretch"
Spacing="4"
@ -54,6 +54,7 @@
<!-- Option Cards -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<ItemsControl
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Padding="8"
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 StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models.Api;
@ -10,27 +12,12 @@ namespace StabilityMatrix.Core.Database;
public class LiteDbContext : ILiteDbContext
{
private readonly ILogger<LiteDbContext> logger;
private readonly ISettingsManager settingsManager;
private readonly DebugOptions debugOptions;
private LiteDatabaseAsync? database;
public LiteDatabaseAsync Database
{
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;
}
}
public LiteDatabaseAsync Database => database ??= CreateDatabase();
// Notification events
public event EventHandler? CivitModelsChanged;
@ -41,12 +28,50 @@ public class LiteDbContext : ILiteDbContext
public ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache => Database.GetCollection<CivitModelQueryCacheEntry>("CivitModelQueryCache");
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.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)
{
var version = await CivitModelVersions.Query()
@ -54,22 +79,26 @@ public class LiteDbContext : ILiteDbContext
.Select(f => f.Hashes)
.Select(hashes => hashes.BLAKE3)
.Any(hash => hash == hashBlake3))
.FirstOrDefaultAsync();
.FirstOrDefaultAsync()
.ConfigureAwait(false);
if (version is null) return (null, null);
var model = await CivitModels.Query()
.Include(m => m.ModelVersions)
.Where(m => m.ModelVersions!
.Select(v => v.Id)
.Any(id => id == version.Id))
.FirstOrDefaultAsync();
.FirstOrDefaultAsync().ConfigureAwait(false);
return (model, version);
}
public async Task<bool> UpsertCivitModelAsync(CivitModel civitModel)
{
// Insert model versions first then model
var versionsUpdated = await CivitModelVersions.UpsertAsync(civitModel.ModelVersions);
var updated = await CivitModels.UpsertAsync(civitModel);
var versionsUpdated = await CivitModelVersions.UpsertAsync(civitModel.ModelVersions).ConfigureAwait(false);
var updated = await CivitModels.UpsertAsync(civitModel).ConfigureAwait(false);
// Notify listeners on any change
var anyUpdated = versionsUpdated > 0 || updated;
if (anyUpdated)
@ -84,8 +113,8 @@ public class LiteDbContext : ILiteDbContext
var civitModelsArray = civitModels.ToArray();
// Get all model versions then insert models
var versions = civitModelsArray.SelectMany(model => model.ModelVersions ?? new());
var versionsUpdated = await CivitModelVersions.UpsertAsync(versions);
var updated = await CivitModels.UpsertAsync(civitModelsArray);
var versionsUpdated = await CivitModelVersions.UpsertAsync(versions).ConfigureAwait(false);
var updated = await CivitModels.UpsertAsync(civitModelsArray).ConfigureAwait(false);
// Notify listeners on any change
var anyUpdated = versionsUpdated > 0 || updated > 0;
if (updated > 0 || versionsUpdated > 0)
@ -98,7 +127,7 @@ public class LiteDbContext : ILiteDbContext
// Add to cache
public async Task<bool> UpsertCivitModelQueryCacheEntryAsync(CivitModelQueryCacheEntry entry)
{
var changed = await CivitModelQueryCache.UpsertAsync(entry);
var changed = await CivitModelQueryCache.UpsertAsync(entry).ConfigureAwait(false);
if (changed)
{
CivitModelsChanged?.Invoke(this, EventArgs.Empty);
@ -107,8 +136,14 @@ public class LiteDbContext : ILiteDbContext
return changed;
}
public Task<GithubCacheEntry?> GetGithubCacheEntry(string cacheKey) =>
GithubCache.FindByIdAsync(cacheKey);
public async Task<GithubCacheEntry?> GetGithubCacheEntry(string cacheKey)
{
if (await GithubCache.FindByIdAsync(cacheKey).ConfigureAwait(false) is { } result)
{
return result;
}
return null;
}
public Task<bool> UpsertGithubCacheEntry(GithubCacheEntry 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
var initialOptions = new Dictionary<string, object>();
// Dict of
var launchArgsDict = launchArgs.ToDictionary(launchArg => launchArg.Name);
// To dictionary ignoring duplicates
var launchArgsDict = launchArgs
.ToLookup(launchArg => launchArg.Name)
.ToDictionary(
group => group.Key,
group => group.First()
);
// Create cards
foreach (var definition in definitions)

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

@ -105,13 +105,13 @@ public abstract class BaseGitPackage : BasePackage
/// <summary>
/// Setup the virtual environment for the package.
/// </summary>
/// <param name="installedPackagePath"></param>
/// <param name="venvName"></param>
/// <returns></returns>
[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)
{
await VenvRunner.DisposeAsync().ConfigureAwait(false);
@ -123,9 +123,9 @@ public abstract class BaseGitPackage : BasePackage
EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables,
};
if (!VenvRunner.Exists())
if (!VenvRunner.Exists() || forceRecreate)
{
await VenvRunner.Setup().ConfigureAwait(false);
await VenvRunner.Setup(forceRecreate).ConfigureAwait(false);
}
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,
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));
var installDir = new DirectoryPath(InstallLocation);
@ -245,7 +245,7 @@ public class VladAutomatic : BaseGitPackage
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));
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.Options;
using StabilityMatrix.Core.Extensions;
@ -19,23 +20,30 @@ public class UpdateHelper : IUpdateHelper
private readonly DebugOptions debugOptions;
private readonly System.Timers.Timer timer = new(TimeSpan.FromMinutes(5));
private string UpdateManifestUrl => debugOptions.UpdateManifestUrl ??
"https://cdn.lykos.ai/update-v2.json";
private string UpdateManifestUrl =>
debugOptions.UpdateManifestUrl ?? "https://cdn.lykos.ai/update-v2.json";
public const string UpdateFolderName = ".StabilityMatrixUpdate";
public static DirectoryPath UpdateFolder => Compat.AppCurrentDir.JoinDir(UpdateFolderName);
public static FilePath ExecutablePath => UpdateFolder.JoinFile(Compat.GetExecutableName());
public UpdateHelper(ILogger<UpdateHelper> logger, IHttpClientFactory httpClientFactory,
IDownloadService downloadService, IOptions<DebugOptions> debugOptions)
public UpdateHelper(
ILogger<UpdateHelper> logger,
IHttpClientFactory httpClientFactory,
IDownloadService downloadService,
IOptions<DebugOptions> debugOptions
)
{
this.logger = logger;
this.httpClientFactory = httpClientFactory;
this.downloadService = downloadService;
this.debugOptions = debugOptions.Value;
timer.Elapsed += async (_, _) => { await CheckForUpdate().ConfigureAwait(false); };
timer.Elapsed += async (_, _) =>
{
await CheckForUpdate().ConfigureAwait(false);
};
}
public async Task StartCheckingForUpdates()
@ -45,18 +53,33 @@ public class UpdateHelper : IUpdateHelper
await CheckForUpdate().ConfigureAwait(false);
}
public async Task DownloadUpdate(UpdateInfo updateInfo,
IProgress<ProgressReport> progress)
public async Task DownloadUpdate(UpdateInfo updateInfo, IProgress<ProgressReport> progress)
{
var downloadUrl = updateInfo.DownloadUrl;
Directory.CreateDirectory(UpdateFolder);
// download the file from URL
await downloadService.DownloadToFileAsync(downloadUrl, ExecutablePath, progress: progress,
httpClientName: "UpdateClient").ConfigureAwait(false);
await downloadService
.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>
/// Data for use in signature verification.
@ -66,10 +89,10 @@ public class UpdateHelper : IUpdateHelper
private static string GetUpdateInfoSignedData(UpdateInfo updateInfo)
{
var channel = updateInfo.Channel.GetStringValue().ToLowerInvariant();
var date = updateInfo.ReleaseDate.ToString("yyyy-MM-ddTHH:mm:ss.ffffffzzz");
return $"{updateInfo.Version};{date};{channel};" +
$"{(int) updateInfo.Type};{updateInfo.DownloadUrl};{updateInfo.ChangelogUrl};" +
$"{updateInfo.HashBlake3}";
var date = FormatDateTimeOffsetInvariant(updateInfo.ReleaseDate);
return $"{updateInfo.Version};{date};{channel};"
+ $"{(int)updateInfo.Type};{updateInfo.DownloadUrl};{updateInfo.ChangelogUrl};"
+ $"{updateInfo.HashBlake3}";
}
private async Task CheckForUpdate()
@ -80,15 +103,19 @@ public class UpdateHelper : IUpdateHelper
var response = await httpClient.GetAsync(UpdateManifestUrl).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
logger.LogWarning("Error while checking for update {StatusCode} - {Content}",
response.StatusCode, await response.Content.ReadAsStringAsync().ConfigureAwait(false));
logger.LogWarning(
"Error while checking for update {StatusCode} - {Content}",
response.StatusCode,
await response.Content.ReadAsStringAsync().ConfigureAwait(false)
);
return;
}
var updateCollection =
await JsonSerializer.DeserializeAsync<UpdateCollection>(
await response.Content.ReadAsStreamAsync()
.ConfigureAwait(false)).ConfigureAwait(false);
var updateCollection = await JsonSerializer
.DeserializeAsync<UpdateCollection>(
await response.Content.ReadAsStreamAsync().ConfigureAwait(false)
)
.ConfigureAwait(false);
if (updateCollection is null)
{
@ -106,7 +133,10 @@ public class UpdateHelper : IUpdateHelper
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;
}
@ -129,8 +159,11 @@ public class UpdateHelper : IUpdateHelper
if (order > 0)
{
// Newer version available
logger.LogInformation("Update available {AppVer} -> {UpdateVer}",
Compat.AppVersion, updateInfo.Version);
logger.LogInformation(
"Update available {AppVer} -> {UpdateVer}",
Compat.AppVersion,
updateInfo.Version
);
EventManager.Instance.OnUpdateAvailable(updateInfo);
return;
}
@ -142,8 +175,11 @@ public class UpdateHelper : IUpdateHelper
// If different, we can update
if (updateHash != appHash)
{
logger.LogInformation("Update available {AppVer} -> {UpdateVer}",
Compat.AppVersion, updateInfo.Version);
logger.LogInformation(
"Update available {AppVer} -> {UpdateVer}",
Compat.AppVersion,
updateInfo.Version
);
EventManager.Instance.OnUpdateAvailable(updateInfo);
return;
}

Loading…
Cancel
Save