Browse Source

Merge pull request #58 from ionite34/checkpoint-browser

pull/5/head
Ionite 1 year ago committed by GitHub
parent
commit
e235e80370
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      StabilityMatrix/Api/ICivitApi.cs
  2. 9
      StabilityMatrix/App.xaml.cs
  3. 209
      StabilityMatrix/CheckpointBrowserPage.xaml
  4. 36
      StabilityMatrix/CheckpointBrowserPage.xaml.cs
  5. 231
      StabilityMatrix/CheckpointManagerPage.xaml
  6. 32
      StabilityMatrix/Converters/BooleanToHiddenVisibleConverter.cs
  7. 5
      StabilityMatrix/Converters/UriToBitmapConverter.cs
  8. 39
      StabilityMatrix/DesignData/MockCheckpointBrowserViewModel.cs
  9. 1
      StabilityMatrix/DesignData/MockCheckpointManagerViewModel.cs
  10. 56
      StabilityMatrix/Extensions/EnumAttributes.cs
  11. 26
      StabilityMatrix/Extensions/EnumConversion.cs
  12. 61
      StabilityMatrix/Helper/FileHash.cs
  13. 76
      StabilityMatrix/Helper/FileTransfers.cs
  14. 12
      StabilityMatrix/Helper/PrerequisiteHelper.cs
  15. 18
      StabilityMatrix/MainWindow.xaml
  16. 13
      StabilityMatrix/Models/Api/CivitCommercialUse.cs
  17. 12
      StabilityMatrix/Models/Api/CivitCreator.cs
  18. 31
      StabilityMatrix/Models/Api/CivitFile.cs
  19. 12
      StabilityMatrix/Models/Api/CivitFileHashes.cs
  20. 15
      StabilityMatrix/Models/Api/CivitFileMetadata.cs
  21. 23
      StabilityMatrix/Models/Api/CivitImage.cs
  22. 25
      StabilityMatrix/Models/Api/CivitMetadata.cs
  23. 10
      StabilityMatrix/Models/Api/CivitMode.cs
  24. 36
      StabilityMatrix/Models/Api/CivitModel.cs
  25. 12
      StabilityMatrix/Models/Api/CivitModelFormat.cs
  26. 13
      StabilityMatrix/Models/Api/CivitModelFpType.cs
  27. 12
      StabilityMatrix/Models/Api/CivitModelSize.cs
  28. 12
      StabilityMatrix/Models/Api/CivitModelStats.cs
  29. 28
      StabilityMatrix/Models/Api/CivitModelType.cs
  30. 37
      StabilityMatrix/Models/Api/CivitModelVersion.cs
  31. 106
      StabilityMatrix/Models/Api/CivitModelsRequest.cs
  32. 12
      StabilityMatrix/Models/Api/CivitModelsResponse.cs
  33. 13
      StabilityMatrix/Models/Api/CivitPeriod.cs
  34. 16
      StabilityMatrix/Models/Api/CivitSortMode.cs
  35. 15
      StabilityMatrix/Models/Api/CivitStats.cs
  36. 80
      StabilityMatrix/Models/CheckpointFile.cs
  37. 106
      StabilityMatrix/Models/CheckpointFolder.cs
  38. 49
      StabilityMatrix/Models/ConnectedModelInfo.cs
  39. 2
      StabilityMatrix/Models/ISharedFolders.cs
  40. 41
      StabilityMatrix/Models/Packages/A3WebUI.cs
  41. 16
      StabilityMatrix/Models/Packages/BaseGitPackage.cs
  42. 3
      StabilityMatrix/Models/SharedFolderType.cs
  43. 7
      StabilityMatrix/Models/SharedFolders.cs
  44. 34
      StabilityMatrix/Services/DownloadService.cs
  45. 5
      StabilityMatrix/Services/IDownloadService.cs
  46. 4
      StabilityMatrix/SettingsPage.xaml
  47. 6
      StabilityMatrix/StabilityMatrix.csproj
  48. 98
      StabilityMatrix/ViewModels/CheckpointBrowserCardViewModel.cs
  49. 129
      StabilityMatrix/ViewModels/CheckpointBrowserViewModel.cs
  50. 8
      StabilityMatrix/ViewModels/CheckpointManagerViewModel.cs
  51. 28
      StabilityMatrix/ViewModels/ProgressViewModel.cs
  52. 8
      StabilityMatrix/ViewModels/SettingsViewModel.cs

14
StabilityMatrix/Api/ICivitApi.cs

@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Refit;
using StabilityMatrix.Models.Api;
namespace StabilityMatrix.Api;
public interface ICivitApi
{
[Get("/api/v1/models")]
Task<CivitModelsResponse> GetModels(CivitModelsRequest request);
[Get("/api/v1/model-versions/by-hash")]
Task<CivitModelVersion> GetModelVersionByHash([Query] string hash);
}

9
StabilityMatrix/App.xaml.cs

@ -74,6 +74,7 @@ namespace StabilityMatrix
serviceCollection.AddTransient<PackageManagerPage>();
serviceCollection.AddTransient<TextToImagePage>();
serviceCollection.AddTransient<CheckpointManagerPage>();
serviceCollection.AddTransient<CheckpointBrowserPage>();
serviceCollection.AddTransient<InstallerWindow>();
serviceCollection.AddTransient<MainWindowViewModel>();
@ -86,6 +87,7 @@ namespace StabilityMatrix
serviceCollection.AddTransient<InstallerViewModel>();
serviceCollection.AddTransient<OneClickInstallViewModel>();
serviceCollection.AddTransient<CheckpointManagerViewModel>();
serviceCollection.AddSingleton<CheckpointBrowserViewModel>();
var settingsManager = new SettingsManager();
serviceCollection.AddSingleton<ISettingsManager>(settingsManager);
@ -136,6 +138,13 @@ namespace StabilityMatrix
c.Timeout = TimeSpan.FromSeconds(2);
})
.AddPolicyHandler(retryPolicy);
serviceCollection.AddRefitClient<ICivitApi>(defaultRefitSettings)
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://civitai.com");
c.Timeout = TimeSpan.FromSeconds(8);
})
.AddPolicyHandler(retryPolicy);
// Logging configuration
var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log.txt");

209
StabilityMatrix/CheckpointBrowserPage.xaml

@ -0,0 +1,209 @@
<Page
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
d:DataContext="{d:DesignInstance designData:MockCheckpointBrowserViewModel,
IsDesignTimeCreatable=True}"
d:DesignHeight="600"
d:DesignWidth="650"
mc:Ignorable="d"
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
x:Class="StabilityMatrix.CheckpointBrowserPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:api="clr-namespace:StabilityMatrix.Models.Api"
xmlns:converters="clr-namespace:StabilityMatrix.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:designData="clr-namespace:StabilityMatrix.DesignData"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:StabilityMatrix.Models"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:viewModels="clr-namespace:StabilityMatrix.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Resources>
<converters:ValueConverterGroup x:Key="InvertAndVisibilitate">
<converters:BoolNegationConverter />
<BooleanToVisibilityConverter />
</converters:ValueConverterGroup>
<converters:UriToBitmapConverter x:Key="UriToBitmapConverter" />
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<DataTemplate DataType="{x:Type viewModels:CheckpointBrowserCardViewModel}" x:Key="CivitModelTemplate">
<ui:Card MaxHeight="450" Width="330">
<StackPanel Orientation="Vertical">
<TextBlock
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Margin="0,0,0,0"
Text="{Binding CivitModel.Name}"
VerticalAlignment="Center" />
<TextBlock
FontSize="11"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Margin="0,2,0,0"
Text="{Binding CivitModel.ModelVersions[0].Name}"
VerticalAlignment="Center" />
<Grid>
<Image
Margin="0,8,0,8"
MaxHeight="300"
Source="{Binding CivitModel.ModelVersions[0].Images[0].Url, Converter={StaticResource UriToBitmapConverter}}"
Stretch="UniformToFill" />
<ui:Button
Appearance="Info"
Command="{Binding OpenModelCommand}"
CommandParameter="{Binding CivitModel}"
HorizontalAlignment="Right"
Margin="0,16,8,0"
VerticalAlignment="Top">
<ui:SymbolIcon Symbol="Open48" />
</ui:Button>
<Rectangle
Fill="#DD000000"
HorizontalAlignment="Stretch"
Margin="0,8,0,8"
VerticalAlignment="Stretch"
Visibility="{Binding ImportCommand.IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
<StackPanel
HorizontalAlignment="Stretch"
Orientation="Vertical"
VerticalAlignment="Center"
Visibility="{Binding ImportCommand.IsRunning, Converter={StaticResource BoolToVisibilityConverter}}">
<ui:ProgressRing
HorizontalAlignment="Center"
IsIndeterminate="False"
Progress="{Binding Value}"
VerticalAlignment="Center" />
<TextBlock
HorizontalAlignment="Center"
Margin="0,8,0,0"
Text="{Binding Text, FallbackValue=Importing...}"
VerticalAlignment="Center" />
</StackPanel>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<ui:Button
Appearance="Primary"
Command="{Binding ImportCommand}"
CommandParameter="{Binding CivitModel}"
Content="Import"
HorizontalAlignment="Stretch"
Margin="0,8,0,0" />
</Grid>
</StackPanel>
</ui:Card>
</DataTemplate>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Margin="8" Orientation="Vertical">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:TextBox
HorizontalAlignment="Stretch"
Margin="8,0,0,0"
PlaceholderText="Query"
Text="{Binding SearchQuery, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<ui:Button
Appearance="Primary"
Command="{Binding SearchModelsCommand}"
Grid.Column="1"
IsDefault="True"
Margin="8,0,8,0"
VerticalAlignment="Stretch"
Width="80">
<StackPanel Orientation="Horizontal">
<ui:ProgressRing
Height="20"
IsIndeterminate="True"
VerticalAlignment="Center"
Visibility="{Binding SearchModelsCommand.IsRunning, Converter={StaticResource BoolToVisibilityConverter}}"
Width="20" />
<TextBlock
Text="Search"
VerticalAlignment="Center"
Visibility="{Binding SearchModelsCommand.IsRunning, Converter={StaticResource InvertAndVisibilitate}}" />
</StackPanel>
</ui:Button>
</Grid>
<DockPanel>
<StackPanel Margin="8" Orientation="Vertical">
<Label Content="Sort" />
<ComboBox
ItemsSource="{Binding AllSortModes}"
MinWidth="100"
SelectedItem="{Binding SortMode}" />
</StackPanel>
<StackPanel Margin="0,8" Orientation="Vertical">
<Label Content="Period" />
<ComboBox
ItemsSource="{Binding AllCivitPeriods}"
MinWidth="100"
SelectedItem="{Binding SelectedPeriod}" />
</StackPanel>
<CheckBox
Content="Show NSFW Content"
FontSize="12"
HorizontalAlignment="Right"
IsChecked="{Binding ShowNsfw, Mode=TwoWay}"
Margin="8,8,8,0" />
</DockPanel>
</StackPanel>
<ui:DynamicScrollViewer CanContentScroll="True" Grid.Row="1">
<ui:VirtualizingGridView
ItemTemplate="{StaticResource CivitModelTemplate}"
ItemsSource="{Binding ModelCards}"
PreviewMouseWheel="VirtualizingGridView_OnPreviewMouseWheel"
SpacingMode="StartAndEndOnly" />
</ui:DynamicScrollViewer>
<StackPanel
Grid.Row="2"
HorizontalAlignment="Center"
Margin="8"
Orientation="Vertical"
Visibility="{Binding HasSearched, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock Margin="0,0,4,4" TextAlignment="Center">
<Run Text="Page" />
<Run Text="{Binding CurrentPageNumber, FallbackValue=1}" />
<Run Text="/" />
<Run Text="{Binding TotalPages, FallbackValue=5}" />
</TextBlock>
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<ui:Button
Command="{Binding PreviousPageCommand}"
IsEnabled="{Binding CanGoToPreviousPage}"
Margin="0,0,8,0">
<ui:SymbolIcon Symbol="ArrowPrevious24" />
</ui:Button>
<ui:Button Command="{Binding NextPageCommand}" IsEnabled="{Binding CanGoToNextPage}">
<ui:SymbolIcon Symbol="ArrowNext24" />
</ui:Button>
</StackPanel>
</StackPanel>
<ui:ProgressRing
Grid.Row="0"
Grid.RowSpan="3"
IsIndeterminate="True"
Visibility="{Binding SearchModelsCommand.IsRunning, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</Page>

36
StabilityMatrix/CheckpointBrowserPage.xaml.cs

@ -0,0 +1,36 @@
using System.Diagnostics;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Effects;
using StabilityMatrix.ViewModels;
using Wpf.Ui.Controls;
namespace StabilityMatrix;
public partial class CheckpointBrowserPage : Page
{
public CheckpointBrowserPage(CheckpointBrowserViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
private void VirtualizingGridView_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Handled) return;
e.Handled = true;
var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = MouseWheelEvent,
Source = sender
};
if (((Control)sender).Parent is UIElement parent)
{
parent.RaiseEvent(eventArg);
}
}
}

231
StabilityMatrix/CheckpointManagerPage.xaml

@ -9,8 +9,10 @@
mc:Ignorable="d"
x:Class="StabilityMatrix.CheckpointManagerPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:converters="clr-namespace:StabilityMatrix.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:designData="clr-namespace:StabilityMatrix.DesignData"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:StabilityMatrix.Models"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
@ -18,31 +20,122 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Resources>
<converters:ValueConverterGroup x:Key="InvertAndVisibleOrHidden">
<converters:BoolNegationConverter />
<converters:BooleanToHiddenVisibleConverter />
</converters:ValueConverterGroup>
<DropShadowEffect
BlurRadius="8"
Color="#FF000000"
Direction="0"
Opacity="0.2"
ShadowDepth="0"
x:Key="TextDropShadowEffect" />
<DataTemplate DataType="{x:Type models:CheckpointFile}" x:Key="CheckpointFileDataTemplate">
<ui:CardAction Height="96" Width="240">
<StackPanel Orientation="Vertical">
<ui:Image
CornerRadius="4"
Margin="4,4,4,8"
MinHeight="256"
MinWidth="200"
Source="{Binding PreviewImage}"
Stretch="UniformToFill"
Visibility="Collapsed"
Width="128" />
<TextBlock
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Margin="0,0,0,0"
Text="{Binding Title}"
VerticalAlignment="Center" />
<TextBlock
FontSize="11"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Margin="0,2,0,0"
Text="{Binding FileName}"
VerticalAlignment="Center" />
</StackPanel>
</ui:CardAction>
<Border>
<i:Interaction.Behaviors>
<i:MouseDragElementBehavior />
</i:Interaction.Behaviors>
<ui:Card
Height="96"
Margin="8"
Width="240">
<ui:Card.ContextMenu>
<ContextMenu>
<MenuItem Header="Rename" />
<MenuItem Command="{Binding DeleteCommand}" Header="Delete" />
</ContextMenu>
</ui:Card.ContextMenu>
<Grid>
<!-- Main contents, hidden when IsLoading is true -->
<Grid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Visibility="{Binding IsLoading, Converter={StaticResource InvertAndVisibleOrHidden}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="0.2*" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Vertical">
<ui:Image
CornerRadius="4"
Margin="4,4,4,8"
MinHeight="256"
MinWidth="200"
Source="{Binding PreviewImage}"
Stretch="UniformToFill"
Visibility="Collapsed"
Width="128" />
<TextBlock
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Margin="0,0,0,0"
Text="{Binding Title}"
VerticalAlignment="Center" />
<TextBlock
FontSize="11"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Margin="0,2,0,0"
Text="{Binding FileName}"
VerticalAlignment="Center" />
</StackPanel>
<Grid Grid.Column="1">
<ui:Button
Background="Transparent"
BorderBrush="Transparent"
FontSize="20"
HorizontalAlignment="Right"
IsEnabled="False"
MaxHeight="48"
MaxWidth="64"
Padding="0"
VerticalAlignment="Top"
Visibility="{Binding IsConnectedModel, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}">
<ui:Button.Icon>
<ui:SymbolIcon
FontSize="12"
Foreground="LightGreen"
Symbol="CloudCheckmark24"
ToolTip="Connected Model" />
</ui:Button.Icon>
</ui:Button>
<ui:Button
Background="Transparent"
BorderBrush="Transparent"
FontSize="20"
HorizontalAlignment="Right"
IsEnabled="False"
MaxHeight="48"
MaxWidth="64"
Padding="0"
VerticalAlignment="Top"
Visibility="Collapsed">
<ui:Button.Icon>
<ui:SymbolIcon
FontSize="12"
Foreground="OrangeRed"
Symbol="CloudArrowUp24"
ToolTip="Update Available" />
</ui:Button.Icon>
</ui:Button>
</Grid>
</Grid>
<!-- Progress ring -->
<ui:ProgressRing
Grid.Row="0"
Height="32"
HorizontalAlignment="Center"
IsEnabled="{Binding IsLoading}"
IsIndeterminate="True"
Padding="0"
VerticalAlignment="Center"
Visibility="{Binding IsLoading, Converter={StaticResource BooleanToVisibilityConverter}}"
Width="32" />
</Grid>
</ui:Card>
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type models:CheckpointFolder}" x:Key="CheckpointFolderGridDataTemplate">
@ -51,12 +144,88 @@
Header="{Binding Title}"
IsExpanded="True"
Margin="8">
<ui:VirtualizingGridView
ItemTemplate="{StaticResource CheckpointFileDataTemplate}"
ItemsSource="{Binding CheckpointFiles}"
Padding="5"
PreviewMouseWheel="VirtualizingGridView_OnPreviewMouseWheel"
SpacingMode="StartAndEndOnly" />
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewDragEnter">
<i:InvokeCommandAction Command="{Binding OnPreviewDragEnterCommand}" />
</i:EventTrigger>
<i:EventTrigger EventName="PreviewDragLeave">
<i:InvokeCommandAction Command="{Binding OnPreviewDragLeaveCommand}" />
</i:EventTrigger>
<i:EventTrigger EventName="PreviewDrop">
<i:InvokeCommandAction Command="{Binding PreviewDropCommand}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
<ui:VirtualizingItemsControl
ItemTemplate="{StaticResource CheckpointFileDataTemplate}"
ItemsSource="{Binding CheckpointFiles}"
Padding="5"
PreviewMouseWheel="VirtualizingGridView_OnPreviewMouseWheel" />
<Border
CornerRadius="8"
Grid.RowSpan="4"
IsEnabled="False"
Name="OnDragBlurBorder"
Visibility="{Binding IsDragBlurEnabled, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}">
<Border.Background>
<SolidColorBrush Color="#EE202020" />
</Border.Background>
</Border>
<Border
BorderThickness="1,1,1,1"
CornerRadius="8"
Grid.RowSpan="4"
IsEnabled="False"
Name="OnDragDashBorder"
Visibility="{Binding IsCurrentDragTarget, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}">
<Border.BorderBrush>
<DrawingBrush
TileMode="Tile"
Viewport="0,0,8,8"
ViewportUnits="Absolute">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="White">
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="0,0,50,50" />
<RectangleGeometry Rect="50,50,50,50" />
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Border.BorderBrush>
</Border>
<VirtualizingStackPanel Orientation="Vertical" VerticalAlignment="Center">
<TextBlock
Effect="{StaticResource TextDropShadowEffect}"
FontSize="24"
HorizontalAlignment="Center"
Name="OnDragText"
Text="Drop a file here to import"
VerticalAlignment="Center"
Visibility="{Binding IsCurrentDragTarget, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}, FallbackValue=Visible}" />
<TextBlock
Effect="{StaticResource TextDropShadowEffect}"
FontSize="18"
HorizontalAlignment="Center"
Name="ImportProgressText"
Text="{Binding Progress.Text, FallbackValue=Importing checkpoint.safetensors}"
VerticalAlignment="Center"
Visibility="{Binding Progress.TextVisibility}" />
<ProgressBar
Effect="{StaticResource TextDropShadowEffect}"
HorizontalAlignment="Stretch"
IsIndeterminate="{Binding Progress.IsIndeterminate, FallbackValue=False}"
Margin="64,8"
Name="ImportProgressBar"
Value="{Binding Progress.Value, FallbackValue=20}"
VerticalAlignment="Center"
Visibility="{Binding Progress.ProgressVisibility, FallbackValue=Visible}" />
</VirtualizingStackPanel>
</Grid>
</Expander>
</DataTemplate>
@ -82,7 +251,7 @@
HorizontalAlignment="Stretch"
ItemTemplate="{StaticResource CheckpointFolderGridDataTemplate}"
ItemsSource="{Binding CheckpointFolders, Mode=OneWay}"
Margin="16,16,16,16" />
Margin="8" />
</StackPanel>
</Grid>
</ui:DynamicScrollViewer>

32
StabilityMatrix/Converters/BooleanToHiddenVisibleConverter.cs

@ -0,0 +1,32 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace StabilityMatrix.Converters;
public class BooleanToHiddenVisibleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var bValue = false;
if (value is bool b)
{
bValue = b;
}
else if (value is bool)
{
var tmp = (bool?) value;
bValue = tmp.Value;
}
return bValue ? Visibility.Visible : Visibility.Hidden;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Visibility visibility)
{
return visibility == Visibility.Visible;
}
return false;
}
}

5
StabilityMatrix/Converters/UriToBitmapConverter.cs

@ -14,6 +14,11 @@ public class UriToBitmapConverter : IValueConverter
return new BitmapImage(uri);
}
if (value is string uriString)
{
return new BitmapImage(new Uri(uriString));
}
return null;
}

39
StabilityMatrix/DesignData/MockCheckpointBrowserViewModel.cs

@ -0,0 +1,39 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using StabilityMatrix.Models.Api;
using StabilityMatrix.ViewModels;
namespace StabilityMatrix.DesignData;
[DesignOnly(true)]
public class MockCheckpointBrowserViewModel : CheckpointBrowserViewModel
{
public MockCheckpointBrowserViewModel() : base(null!, null!, null!)
{
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
{
new (null!, null!, null!)
{
CivitModel = new()
{
Name = "bb95 Furry Mix",
ModelVersions = new[]
{
new CivitModelVersion
{
Name = "v7.0",
Images = new[]
{
new CivitImage
{
Url =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/1547f350-461a-4cd0-a753-0544aa81e4fc/width=450/00000-4137473915.jpeg"
}
}
}
}
}
}
};
}
}

1
StabilityMatrix/DesignData/MockCheckpointManagerViewModel.cs

@ -40,6 +40,7 @@ public class MockCheckpointManagerViewModel : CheckpointManagerViewModel
new()
{
Title = "Lora",
IsCurrentDragTarget = true,
CheckpointFiles = new()
{
new()

56
StabilityMatrix/Extensions/EnumAttributes.cs

@ -0,0 +1,56 @@
using System;
using System.Linq;
using System.Windows.Ink;
namespace StabilityMatrix.Extensions;
public static class EnumAttributeExtensions
{
private static T? GetAttributeValue<T>(Enum value)
{
var type = value.GetType();
var fieldInfo = type.GetField(value.ToString());
// Get the string value attributes
var attribs = fieldInfo?.GetCustomAttributes(typeof(T), false) as T[];
// Return the first if there was a match.
return attribs?.Length > 0 ? attribs[0] : default;
}
/// <summary>
/// Gets the StringValue field attribute on a given enum value.
/// If not found, returns the enum value itself as a string.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static string GetStringValue(this Enum value)
{
var attr = GetAttributeValue<StringValueAttribute>(value)?.StringValue;
return attr ?? Enum.GetName(value.GetType(), value)!;
}
/// <summary>
/// Gets the Description field attribute on a given enum value.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static string? GetDescription(this Enum value)
{
return GetAttributeValue<DescriptionAttribute>(value)?.Description;
}
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class StringValueAttribute : Attribute
{
public string StringValue { get; }
public StringValueAttribute(string value) {
StringValue = value;
}
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class DescriptionAttribute : Attribute
{
public string Description { get; }
public DescriptionAttribute(string value) {
Description = value;
}
}

26
StabilityMatrix/Extensions/EnumConversion.cs

@ -0,0 +1,26 @@
using System;
namespace StabilityMatrix.Extensions;
public static class EnumConversionExtensions
{
public static T? ConvertTo<T>(this Enum value) where T : Enum
{
var type = value.GetType();
var fieldInfo = type.GetField(value.ToString());
// Get the string value attributes
var attribs = fieldInfo?.GetCustomAttributes(typeof(ConvertToAttribute<T>), false) as ConvertToAttribute<T>[];
// Return the first if there was a match.
return attribs?.Length > 0 ? attribs[0].ConvertToEnum : default;
}
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class ConvertToAttribute<T> : Attribute where T : Enum
{
public T ConvertToEnum { get; }
public ConvertToAttribute(T toEnum)
{
ConvertToEnum = toEnum;
}
}

61
StabilityMatrix/Helper/FileHash.cs

@ -0,0 +1,61 @@
using System;
using System.Buffers;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
using StabilityMatrix.Models;
namespace StabilityMatrix.Helper;
public static class FileHash
{
public static async Task<string> GetHashAsync(HashAlgorithm hashAlgorithm, Stream stream, byte[] buffer, Action<ulong>? progress = default)
{
ulong totalBytesRead = 0;
using (hashAlgorithm)
{
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer)) != 0)
{
totalBytesRead += (ulong) bytesRead;
hashAlgorithm.TransformBlock(buffer, 0, bytesRead, null, 0);
progress?.Invoke(totalBytesRead);
}
hashAlgorithm.TransformFinalBlock(buffer, 0, 0);
var hash = hashAlgorithm.Hash;
if (hash == null || hash.Length == 0)
{
throw new InvalidOperationException("Hash algorithm did not produce a hash.");
}
return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
}
}
public static async Task<string> GetSha256Async(string filePath, IProgress<ProgressReport>? progress = default)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"Could not find file: {filePath}");
}
var totalBytes = Convert.ToUInt64(new FileInfo(filePath).Length);
var shared = ArrayPool<byte>.Shared;
var buffer = shared.Rent((int) FileTransfers.GetBufferSize(totalBytes));
try
{
await using var stream = File.OpenRead(filePath);
var hash = await GetHashAsync(SHA256.Create(), stream, buffer, totalBytesRead =>
{
progress?.Report(new ProgressReport(totalBytesRead, totalBytes));
});
return hash;
}
finally
{
shared.Return(buffer);
}
}
}

76
StabilityMatrix/Helper/FileTransfers.cs

@ -0,0 +1,76 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using StabilityMatrix.Models;
namespace StabilityMatrix.Helper;
public static class FileTransfers
{
/// <summary>
/// Determines suitable buffer size based on stream length.
/// </summary>
/// <param name="totalBytes"></param>
/// <returns></returns>
public static ulong GetBufferSize(ulong totalBytes) => totalBytes switch
{
< Size.MiB => 8 * Size.KiB,
< 100 * Size.MiB => 16 * Size.KiB,
< 500 * Size.MiB => Size.MiB,
< Size.GiB => 16 * Size.MiB,
_ => 32 * Size.MiB
};
public static async Task CopyFiles(Dictionary<string, string> files, IProgress<ProgressReport>? fileProgress = default, IProgress<ProgressReport>? totalProgress = default)
{
var totalFiles = files.Count;
var currentFiles = 0;
var totalSize = Convert.ToUInt64(files.Keys.Select(x => new FileInfo(x).Length).Sum());
var totalRead = 0ul;
foreach(var (sourcePath, destPath) in files)
{
var totalReadForFile = 0ul;
await using var outStream = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.Read);
await using var inStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read);
var fileSize = (ulong) inStream.Length;
var fileName = Path.GetFileName(sourcePath);
currentFiles++;
await CopyStream(inStream , outStream, fileReadBytes =>
{
var lastRead = totalReadForFile;
totalReadForFile = Convert.ToUInt64(fileReadBytes);
totalRead += totalReadForFile - lastRead;
fileProgress?.Report(new ProgressReport(totalReadForFile, fileSize, fileName, $"{currentFiles}/{totalFiles}"));
totalProgress?.Report(new ProgressReport(totalRead, totalSize, fileName, $"{currentFiles}/{totalFiles}"));
} );
}
}
private static async Task CopyStream(Stream from, Stream to, Action<long> progress)
{
var shared = ArrayPool<byte>.Shared;
var bufferSize = (int) GetBufferSize((ulong) from.Length);
var buffer = shared.Rent(bufferSize);
var totalRead = 0L;
try
{
while (totalRead < from.Length)
{
var read = await from.ReadAsync(buffer.AsMemory(0, bufferSize));
await to.WriteAsync(buffer.AsMemory(0, read));
totalRead += read;
progress(totalRead);
}
}
finally
{
shared.Return(buffer);
}
}
}

12
StabilityMatrix/Helper/PrerequisiteHelper.cs

@ -59,13 +59,13 @@ public class PrerequisiteHelper : IPrerequisiteHelper
if (!File.Exists(PortableGitDownloadPath))
{
downloadService.DownloadProgressChanged += OnDownloadProgressChanged;
downloadService.DownloadComplete += OnDownloadComplete;
var progress = new Progress<ProgressReport>(progress =>
{
OnDownloadProgressChanged(this, progress);
});
await downloadService.DownloadToFileAsync(portableGitUrl, PortableGitDownloadPath);
downloadService.DownloadProgressChanged -= OnDownloadProgressChanged;
downloadService.DownloadComplete -= OnDownloadComplete;
await downloadService.DownloadToFileAsync(portableGitUrl, PortableGitDownloadPath, progress: progress);
OnDownloadComplete(this, new ProgressReport(progress: 1f));
}
await UnzipGit();

18
StabilityMatrix/MainWindow.xaml

@ -1,19 +1,20 @@
<ui:FluentWindow
ExtendsContentIntoTitleBar="True"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Height="700"
Height="750"
Icon="Assets/Icon.ico"
Loaded="MainWindow_OnLoaded"
Title="Stability Matrix"
Width="1100"
WindowBackdropType="Acrylic"
d:DesignHeight="500"
d:DesignWidth="700"
WindowStartupLocation="CenterScreen"
d:DataContext="{d:DesignInstance Type=viewModels:MainWindowViewModel,
IsDesignTimeCreatable=True}"
d:DesignHeight="500"
d:DesignWidth="700"
mc:Ignorable="d"
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
mc:Ignorable="d"
x:Class="StabilityMatrix.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@ -46,9 +47,9 @@
x:Name="RootNavigation">
<ui:NavigationView.Header>
<ui:BreadcrumbBar
FontSize="28"
FontSize="24"
FontWeight="DemiBold"
Margin="42,32,0,16" />
Margin="24,16,0,16" />
</ui:NavigationView.Header>
<ui:NavigationView.MenuItems>
<ui:NavigationViewItem
@ -69,6 +70,11 @@
<ui:SymbolIcon Symbol="Notebook24" />
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="Get Checkpoints" TargetPageType="{x:Type local:CheckpointBrowserPage}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="NotebookAdd24" />
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="Text2Image (Beta™)" TargetPageType="{x:Type local:TextToImagePage}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Image24" />

13
StabilityMatrix/Models/Api/CivitCommercialUse.cs

@ -0,0 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CivitCommercialUse
{
None,
Image,
Rent,
Sell
}

12
StabilityMatrix/Models/Api/CivitCreator.cs

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
public class CivitCreator
{
[JsonPropertyName("username")]
public string Username { get; set; }
[JsonPropertyName("image")]
public string? Image { get; set; }
}

31
StabilityMatrix/Models/Api/CivitFile.cs

@ -0,0 +1,31 @@
using System;
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
public class CivitFile
{
[JsonPropertyName("sizeKb")]
public double SizeKb { get; set; }
[JsonPropertyName("pickleScanResult")]
public string PickleScanResult { get; set; }
[JsonPropertyName("virusScanResult")]
public string VirusScanResult { get; set; }
[JsonPropertyName("scannedAt")]
public DateTime? ScannedAt { get; set; }
[JsonPropertyName("metadata")]
public CivitFileMetadata Metadata { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("downloadUrl")]
public string DownloadUrl { get; set; }
[JsonPropertyName("hashes")]
public CivitFileHashes Hashes { get; set; }
}

12
StabilityMatrix/Models/Api/CivitFileHashes.cs

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
public class CivitFileHashes
{
public string? SHA256 { get; set; }
public string? CRC32 { get; set; }
public string? BLAKE3 { get; set; }
}

15
StabilityMatrix/Models/Api/CivitFileMetadata.cs

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
public class CivitFileMetadata
{
[JsonPropertyName("fp")]
public CivitModelFpType? Fp { get; set; }
[JsonPropertyName("size")]
public CivitModelSize? Size { get; set; }
[JsonPropertyName("format")]
public CivitModelFormat? Format { get; set; }
}

23
StabilityMatrix/Models/Api/CivitImage.cs

@ -0,0 +1,23 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
public class CivitImage
{
[JsonPropertyName("url")]
public string Url { get; set; }
[JsonPropertyName("nsfw")]
public string Nsfw { get; set; }
[JsonPropertyName("width")]
public int Width { get; set; }
[JsonPropertyName("height")]
public int Height { get; set; }
[JsonPropertyName("hash")]
public string Hash { get; set; }
// TODO: "meta" ( object? )
}

25
StabilityMatrix/Models/Api/CivitMetadata.cs

@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
public class CivitMetadata
{
[JsonPropertyName("totalItems")]
public int TotalItems { get; set; }
[JsonPropertyName("currentPage")]
public int CurrentPage { get; set; }
[JsonPropertyName("pageSize")]
public int PageSize { get; set; }
[JsonPropertyName("totalPages")]
public int TotalPages { get; set; }
[JsonPropertyName("nextPage")]
public string? NextPage { get; set; }
[JsonPropertyName("prevPage")]
public string? PrevPage { get; set; }
}

10
StabilityMatrix/Models/Api/CivitMode.cs

@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CivitMode
{
Archived,
TakenDown
}

36
StabilityMatrix/Models/Api/CivitModel.cs

@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
public class CivitModel
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("type")]
public CivitModelType Type { get; set; }
[JsonPropertyName("nsfw")]
public bool Nsfw { get; set; }
[JsonPropertyName("tags")]
public string[] Tags { get; set; }
[JsonPropertyName("mode")]
public CivitMode? Mode { get; set; }
[JsonPropertyName("creator")]
public CivitCreator Creator { get; set; }
[JsonPropertyName("stats")]
public CivitModelStats Stats { get; set; }
[JsonPropertyName("modelVersions")]
public CivitModelVersion[] ModelVersions { get; set; }
}

12
StabilityMatrix/Models/Api/CivitModelFormat.cs

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CivitModelFormat
{
SafeTensor,
PickleTensor,
Other
}

13
StabilityMatrix/Models/Api/CivitModelFpType.cs

@ -0,0 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
[JsonConverter(typeof(JsonStringEnumConverter))]
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum CivitModelFpType
{
fp16,
fp32
}

12
StabilityMatrix/Models/Api/CivitModelSize.cs

@ -0,0 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
[JsonConverter(typeof(JsonStringEnumConverter))]
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum CivitModelSize
{
full,
pruned,
}

12
StabilityMatrix/Models/Api/CivitModelStats.cs

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
public class CivitModelStats : CivitStats
{
[JsonPropertyName("favoriteCount")]
public int FavoriteCount { get; set; }
[JsonPropertyName("commentCount")]
public int CommentCount { get; set; }
}

28
StabilityMatrix/Models/Api/CivitModelType.cs

@ -0,0 +1,28 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using StabilityMatrix.Extensions;
namespace StabilityMatrix.Models.Api;
[JsonConverter(typeof(JsonStringEnumConverter))]
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum CivitModelType
{
[ConvertTo<SharedFolderType>(SharedFolderType.StableDiffusion)]
Checkpoint,
[ConvertTo<SharedFolderType>(SharedFolderType.TextualInversion)]
TextualInversion,
[ConvertTo<SharedFolderType>(SharedFolderType.Hypernetwork)]
Hypernetwork,
AestheticGradient,
[ConvertTo<SharedFolderType>(SharedFolderType.Lora)]
LORA,
[ConvertTo<SharedFolderType>(SharedFolderType.ControlNet)]
Controlnet,
Poses,
[ConvertTo<SharedFolderType>(SharedFolderType.StableDiffusion)]
Model,
[ConvertTo<SharedFolderType>(SharedFolderType.LyCORIS)]
LoCon
}

37
StabilityMatrix/Models/Api/CivitModelVersion.cs

@ -0,0 +1,37 @@
using System;
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
public class CivitModelVersion
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("downloadUrl")]
public string DownloadUrl { get; set; }
[JsonPropertyName("trainedWords")]
public string[] TrainedWords { get; set; }
[JsonPropertyName("baseModel")]
public string? BaseModel { get; set; }
[JsonPropertyName("files")]
public CivitFile[] Files { get; set; }
[JsonPropertyName("images")]
public CivitImage[] Images { get; set; }
[JsonPropertyName("stats")]
public CivitModelStats Stats { get; set; }
}

106
StabilityMatrix/Models/Api/CivitModelsRequest.cs

@ -0,0 +1,106 @@
using System.Text.Json.Serialization;
using Refit;
namespace StabilityMatrix.Models.Api;
public class CivitModelsRequest
{
/// <summary>
/// The number of results to be returned per page. This can be a number between 1 and 200. By default, each page will return 100 results
/// </summary>
[AliasAs("limit")]
public int? Limit { get; set; }
/// <summary>
/// The page from which to start fetching models
/// </summary>
[AliasAs("page")]
public int? Page { get; set; }
/// <summary>
/// Search query to filter models by name
/// </summary>
[AliasAs("query")]
public string? Query { get; set; }
/// <summary>
/// Search query to filter models by tag
/// </summary>
[AliasAs("tag")]
public string? Tag { get; set; }
/// <summary>
/// Search query to filter models by user
/// </summary>
[AliasAs("username")]
public string? Username { get; set; }
/// <summary>
/// The type of model you want to filter with. If none is specified, it will return all types
/// </summary>
[AliasAs("types")]
public CivitModelType[]? Types { get; set; }
/// <summary>
/// The order in which you wish to sort the results
/// </summary>
[AliasAs("sort")]
public CivitSortMode? Sort { get; set; }
/// <summary>
/// The time frame in which the models will be sorted
/// </summary>
[AliasAs("period")]
public CivitPeriod? Period { get; set; }
/// <summary>
/// The rating you wish to filter the models with. If none is specified, it will return models with any rating
/// </summary>
[AliasAs("rating")]
public int? Rating { get; set; }
/// <summary>
/// Filter to models that require or don't require crediting the creator
/// <remarks>Requires Authentication</remarks>
/// </summary>
[AliasAs("favorites")]
public bool? Favorites { get; set; }
/// <summary>
/// Filter to hidden models of the authenticated user
/// <remarks>Requires Authentication</remarks>
/// </summary>
[AliasAs("hidden")]
public bool? Hidden { get; set; }
/// <summary>
/// Only include the primary file for each model (This will use your preferred format options if you use an API token or session cookie)
/// </summary>
[AliasAs("primaryFileOnly")]
public bool? PrimaryFileOnly { get; set; }
/// <summary>
/// Filter to models that allow or don't allow creating derivatives
/// </summary>
[AliasAs("allowDerivatives")]
public bool? AllowDerivatives { get; set; }
/// <summary>
/// Filter to models that allow or don't allow derivatives to have a different license
/// </summary>
[AliasAs("allowDifferentLicenses")]
public bool? AllowDifferentLicenses { get; set; }
/// <summary>
/// Filter to models based on their commercial permissions
/// </summary>
[AliasAs("allowCommercialUse")]
public CivitCommercialUse? AllowCommercialUse { get; set; }
/// <summary>
/// If false, will return safer images and hide models that don't have safe images
/// </summary>
[AliasAs("nsfw")]
public string? Nsfw { get; set; }
}

12
StabilityMatrix/Models/Api/CivitModelsResponse.cs

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
public class CivitModelsResponse
{
[JsonPropertyName("items")]
public CivitModel[]? Items { get; set; }
[JsonPropertyName("metadata")]
public CivitMetadata? Metadata { get; set; }
}

13
StabilityMatrix/Models/Api/CivitPeriod.cs

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CivitPeriod
{
AllTime,
Year,
Month,
Week,
Day
}

16
StabilityMatrix/Models/Api/CivitSortMode.cs

@ -0,0 +1,16 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CivitSortMode
{
[EnumMember(Value = "Highest Rated")]
HighestRated,
[EnumMember(Value = "Most Downloaded")]
MostDownloaded,
[EnumMember(Value = "Newest")]
Newest
}

15
StabilityMatrix/Models/Api/CivitStats.cs

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Models.Api;
public class CivitStats
{
[JsonPropertyName("downloadCount")]
public int DownloadCount { get; set; }
[JsonPropertyName("ratingCount")]
public int RatingCount { get; set; }
[JsonPropertyName("rating")]
public double Rating { get; set; }
}

80
StabilityMatrix/Models/CheckpointFile.cs

@ -1,14 +1,23 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NLog;
namespace StabilityMatrix.Models;
public class CheckpointFile
public partial class CheckpointFile : ObservableObject
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
// Event for when this file is deleted
public event EventHandler<CheckpointFile>? Deleted;
/// <summary>
/// Absolute path to the checkpoint file.
/// </summary>
@ -17,25 +26,68 @@ public class CheckpointFile
/// <summary>
/// Custom title for UI.
/// </summary>
public string Title { get; init; } = string.Empty;
[ObservableProperty] private string title = string.Empty;
public string? PreviewImagePath { get; set; }
public BitmapImage? PreviewImage { get; set; }
public bool IsPreviewImageLoaded => PreviewImage != null;
[ObservableProperty] private ConnectedModelInfo? connectedModel;
public bool IsConnectedModel => ConnectedModel != null;
[ObservableProperty] private bool isLoading;
public string FileName => Path.GetFileName(FilePath);
private static readonly string[] SupportedCheckpointExtensions = { ".safetensors", ".pt" };
private static readonly string[] SupportedCheckpointExtensions = { ".safetensors", ".pt", ".ckpt", ".pth" };
private static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" };
partial void OnConnectedModelChanged(ConnectedModelInfo? value)
{
if (value == null) return;
// Update title, first check user defined, then connected model name
Title = value.UserTitle ?? value.ModelName;
}
[RelayCommand]
private async Task DeleteAsync()
{
if (File.Exists(FilePath))
{
// Start progress ring
IsLoading = true;
var timer = Stopwatch.StartNew();
try
{
await Task.Run(() => File.Delete(FilePath));
if (PreviewImagePath != null && File.Exists(PreviewImagePath))
{
await Task.Run(() => File.Delete(PreviewImagePath));
}
// If it was too fast, wait a bit to show progress ring
var targetDelay = new Random().Next(200, 500);
var elapsed = timer.ElapsedMilliseconds;
if (elapsed < targetDelay)
{
await Task.Delay(targetDelay - (int) elapsed);
}
}
catch (IOException e)
{
Logger.Error(e, $"Failed to delete checkpoint file: {FilePath}");
IsLoading = false;
return; // Don't delete from collection
}
}
Deleted?.Invoke(this, this);
}
/// <summary>
/// Indexes directory and yields all checkpoint files.
/// First we match all files with supported extensions.
/// If found, we also look for
/// - {filename}.preview.{image-extensions}
/// - {filename}.preview.{image-extensions} (preview image)
/// - {filename}.cm-info.json (connected model info)
/// </summary>
public static IEnumerable<CheckpointFile> FromDirectoryIndex(string directory, SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
@ -53,6 +105,22 @@ public class CheckpointFile
FilePath = Path.Combine(directory, file),
};
// Check for connected model info
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file);
var cmInfoPath = $"{fileNameWithoutExtension}.cm-info.json";
if (files.ContainsKey(cmInfoPath))
{
try
{
var jsonData = File.ReadAllText(Path.Combine(directory, cmInfoPath));
checkpointFile.ConnectedModel = ConnectedModelInfo.FromJson(jsonData);
}
catch (IOException e)
{
Debug.WriteLine($"Failed to parse {cmInfoPath}: {e}");
}
}
// Check for preview image
var previewImage = SupportedImageExtensions.Select(ext => $"{checkpointFile.FileName}.preview.{ext}").FirstOrDefault(files.ContainsKey);
if (previewImage != null)

106
StabilityMatrix/Models/CheckpointFolder.cs

@ -1,10 +1,19 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Helper;
using StabilityMatrix.ViewModels;
namespace StabilityMatrix.Models;
public class CheckpointFolder
public partial class CheckpointFolder : ObservableObject
{
/// <summary>
/// Absolute path to the folder.
@ -16,8 +25,103 @@ public class CheckpointFolder
/// </summary>
public string Title { get; init; } = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsDragBlurEnabled))]
private bool isCurrentDragTarget;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsDragBlurEnabled))]
private bool isImportInProgress;
public bool IsDragBlurEnabled => IsCurrentDragTarget || IsImportInProgress;
public ProgressViewModel Progress { get; } = new();
public ObservableCollection<CheckpointFile> CheckpointFiles { get; set; } = new();
public RelayCommand OnPreviewDragEnterCommand => new(() => IsCurrentDragTarget = true);
public RelayCommand OnPreviewDragLeaveCommand => new(() => IsCurrentDragTarget = false);
public CheckpointFolder()
{
CheckpointFiles.CollectionChanged += OnCheckpointFilesChanged;
}
// On collection changes
private void OnCheckpointFilesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems == null) return;
// On new added items, add event handler for deletion
foreach (CheckpointFile item in e.NewItems)
{
item.Deleted += OnCheckpointFileDelete;
}
}
/// <summary>
/// Handler for CheckpointFile requesting to be deleted from the collection.
/// </summary>
/// <param name="sender"></param>
/// <param name="file"></param>
private void OnCheckpointFileDelete(object? sender, CheckpointFile file)
{
Application.Current.Dispatcher.Invoke(() => CheckpointFiles.Remove(file));
}
[RelayCommand]
private async Task OnPreviewDropAsync(DragEventArgs e)
{
IsImportInProgress = true;
IsCurrentDragTarget = false;
var files = e.Data.GetData(DataFormats.FileDrop) as string[];
if (files == null || files.Length < 1)
{
IsImportInProgress = false;
return;
}
await ImportFilesAsync(files);
}
/// <summary>
/// Imports files to the folder. Reports progress to instance properties.
/// </summary>
public async Task ImportFilesAsync(IEnumerable<string> files)
{
Progress.IsIndeterminate = true;
Progress.IsProgressVisible = true;
var copyPaths = files.ToDictionary(k => k, v => Path.Combine(DirectoryPath, Path.GetFileName(v)));
var progress = new Progress<ProgressReport>(report =>
{
Progress.IsIndeterminate = false;
Progress.Value = report.Percentage;
// For multiple files, add count
Progress.Text = copyPaths.Count > 1 ? $"Importing {report.Title} ({report.Message})" : $"Importing {report.Title}";
});
await FileTransfers.CopyFiles(copyPaths, progress);
Progress.Value = 100;
Progress.Text = "Import complete";
await IndexAsync();
DelayedClearProgress(TimeSpan.FromSeconds(1));
}
/// <summary>
/// Clears progress after a delay.
/// </summary>
private void DelayedClearProgress(TimeSpan delay)
{
Task.Delay(delay).ContinueWith(_ =>
{
IsImportInProgress = false;
Progress.IsProgressVisible = false;
Progress.Value = 0;
Progress.Text = string.Empty;
});
}
/// <summary>
/// Indexes the folder for checkpoint files.
/// </summary>

49
StabilityMatrix/Models/ConnectedModelInfo.cs

@ -0,0 +1,49 @@
using System;
using System.Text.Json;
using StabilityMatrix.Extensions;
using StabilityMatrix.Models.Api;
namespace StabilityMatrix.Models;
public class ConnectedModelInfo
{
public int ModelId { get; set; }
public string ModelName { get; set; }
public string ModelDescription { get; set; }
public bool Nsfw { get; set; }
public string[] Tags { get; set; }
public CivitModelType ModelType { get; set; }
public int VersionId { get; set; }
public string VersionName { get; set; }
public string VersionDescription { get; set; }
public string? BaseModel { get; set; }
public CivitFileMetadata FileMetadata { get; set; }
public DateTime ImportedAt { get; set; }
public CivitFileHashes Hashes { get; set; }
// User settings
public string? UserTitle { get; set; }
public string? ThumbnailImageUrl { get; set; }
public ConnectedModelInfo(CivitModel civitModel, CivitModelVersion civitModelVersion, CivitFile civitFile, DateTime importedAt)
{
ModelId = civitModel.Id;
ModelName = civitModel.Name;
ModelDescription = civitModel.Description;
Nsfw = civitModel.Nsfw;
Tags = civitModel.Tags;
ModelType = civitModel.Type;
VersionId = civitModelVersion.Id;
VersionName = civitModelVersion.Name;
VersionDescription = civitModelVersion.Description;
ImportedAt = importedAt;
BaseModel = civitModelVersion.BaseModel;
FileMetadata = civitFile.Metadata;
Hashes = civitFile.Hashes;
}
public static ConnectedModelInfo? FromJson(string json)
{
return JsonSerializer.Deserialize<ConnectedModelInfo>(json);
}
}

2
StabilityMatrix/Models/ISharedFolders.cs

@ -4,7 +4,5 @@ namespace StabilityMatrix.Models;
public interface ISharedFolders
{
string SharedFoldersPath { get; }
string SharedFolderTypeToName(SharedFolderType folderType);
void SetupLinksForPackage(BasePackage basePackage, string installPath);
}

41
StabilityMatrix/Models/Packages/A3WebUI.cs

@ -7,6 +7,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using StabilityMatrix.Helper;
using StabilityMatrix.Helper.Cache;
using StabilityMatrix.Python;
using StabilityMatrix.Services;
namespace StabilityMatrix.Models.Packages;
@ -40,6 +41,9 @@ public class A3WebUI : BaseGitPackage
[SharedFolderType.VAE] = "models/VAE",
[SharedFolderType.DeepDanbooru] = "models/deepbooru",
[SharedFolderType.Karlo] = "models/karlo",
[SharedFolderType.TextualInversion] = "embeddings",
[SharedFolderType.Hypernetwork] = "models/hypernetworks",
[SharedFolderType.ControlNet] = "models/ControlNet"
};
public override List<LaunchOptionDefinition> LaunchOptions => new()
@ -97,7 +101,7 @@ public class A3WebUI : BaseGitPackage
var allReleases = await GetAllReleases();
return allReleases.Select(r => new PackageVersion {TagName = r.TagName!, ReleaseNotesMarkdown = r.Body});
}
else // branch mode1
else // branch mode
{
var allBranches = await GetAllBranches();
return allBranches.Select(b => new PackageVersion
@ -108,6 +112,41 @@ public class A3WebUI : BaseGitPackage
}
}
public override async Task InstallPackage(bool isUpdate = false)
{
UnzipPackage(isUpdate);
OnInstallProgressChanged(-1); // Indeterminate progress bar
Logger.Debug("Setting up venv");
await SetupVenv(InstallLocation);
var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv"));
void HandleConsoleOutput(string? s)
{
Debug.WriteLine($"venv stdout: {s}");
OnConsoleOutput(s);
}
// install prereqs
await venvRunner.PipInstall(venvRunner.GetTorchInstallCommand(), InstallLocation, HandleConsoleOutput);
if (HardwareHelper.HasNvidiaGpu())
{
await venvRunner.PipInstall("xformers", InstallLocation, HandleConsoleOutput);
}
await venvRunner.PipInstall("-r requirements.txt", InstallLocation, HandleConsoleOutput);
Logger.Debug("Finished installing requirements");
if (isUpdate)
{
OnUpdateComplete("Update complete");
}
else
{
OnInstallComplete("Install complete");
}
}
public override async Task RunPackage(string installedPackagePath, string arguments)
{
await SetupVenv(installedPackagePath);

16
StabilityMatrix/Models/Packages/BaseGitPackage.cs

@ -108,19 +108,13 @@ public abstract class BaseGitPackage : BasePackage
Directory.CreateDirectory(DownloadLocation.Replace($"{Name}.zip", ""));
}
void DownloadProgressHandler(object? _, ProgressReport progress) =>
var progress = new Progress<ProgressReport>(progress =>
{
DownloadServiceOnDownloadProgressChanged(progress, isUpdate);
});
void DownloadFinishedHandler(object? _, ProgressReport downloadLocation) =>
DownloadServiceOnDownloadFinished(downloadLocation, isUpdate);
DownloadService.DownloadProgressChanged += DownloadProgressHandler;
DownloadService.DownloadComplete += DownloadFinishedHandler;
await DownloadService.DownloadToFileAsync(downloadUrl, DownloadLocation);
DownloadService.DownloadProgressChanged -= DownloadProgressHandler;
DownloadService.DownloadComplete -= DownloadFinishedHandler;
await DownloadService.DownloadToFileAsync(downloadUrl, DownloadLocation, progress: progress);
DownloadServiceOnDownloadFinished(new ProgressReport(100, "Download Complete"), isUpdate);
return version;
}

3
StabilityMatrix/Models/SharedFolderType.cs

@ -16,4 +16,7 @@ public enum SharedFolderType
ApproxVAE,
Karlo,
DeepDanbooru,
TextualInversion,
Hypernetwork,
ControlNet
}

7
StabilityMatrix/Models/SharedFolders.cs

@ -2,6 +2,7 @@
using System.IO;
using NCode.ReparsePoints;
using NLog;
using StabilityMatrix.Extensions;
using StabilityMatrix.Models.Packages;
namespace StabilityMatrix.Models;
@ -10,11 +11,11 @@ public class SharedFolders : ISharedFolders
{
private const string SharedFoldersName = "Models";
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
public string SharedFoldersPath { get; } =
public static string SharedFoldersPath =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StabilityMatrix",
SharedFoldersName);
public string SharedFolderTypeToName(SharedFolderType folderType)
public static string SharedFolderTypeToName(SharedFolderType folderType)
{
return Enum.GetName(typeof(SharedFolderType), folderType)!;
}
@ -30,7 +31,7 @@ public class SharedFolders : ISharedFolders
var provider = ReparsePointFactory.Provider;
foreach (var (folderType, relativePath) in sharedFolders)
{
var source = Path.GetFullPath(Path.Combine(SharedFoldersPath, SharedFolderTypeToName(folderType)));
var source = Path.GetFullPath(Path.Combine(SharedFoldersPath, folderType.GetStringValue()));
var destination = Path.GetFullPath(Path.Combine(installPath, relativePath));
// Create source folder if it doesn't exist
if (!Directory.Exists(source))

34
StabilityMatrix/Services/DownloadService.cs

@ -5,6 +5,7 @@ using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Polly.Contrib.WaitAndRetry;
using StabilityMatrix.Models;
namespace StabilityMatrix.Services;
@ -20,10 +21,8 @@ public class DownloadService : IDownloadService
this.httpClientFactory = httpClientFactory;
}
public event EventHandler<ProgressReport>? DownloadProgressChanged;
public event EventHandler<ProgressReport>? DownloadComplete;
public async Task DownloadToFileAsync(string downloadUrl, string downloadLocation, int bufferSize = ushort.MaxValue)
public async Task DownloadToFileAsync(string downloadUrl, string downloadLocation, int bufferSize = ushort.MaxValue,
IProgress<ProgressReport>? progress = null)
{
using var client = httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromMinutes(5);
@ -31,26 +30,28 @@ public class DownloadService : IDownloadService
await using var file = new FileStream(downloadLocation, FileMode.Create, FileAccess.Write, FileShare.None);
long contentLength = 0;
var retryCount = 0;
var response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
contentLength = response.Content.Headers.ContentLength ?? 0;
while (contentLength == 0 && retryCount++ < 5)
var delays = Backoff.DecorrelatedJitterBackoffV2(
TimeSpan.FromMilliseconds(50), retryCount: 3);
foreach (var delay in delays)
{
if (contentLength > 0) break;
logger.LogDebug("Retrying get-headers for content-length");
Thread.Sleep(50);
await Task.Delay(delay);
response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
contentLength = response.Content.Headers.ContentLength ?? 0;
}
var isIndeterminate = contentLength == 0;
await using var stream = await response.Content.ReadAsStreamAsync();
var totalBytesRead = 0;
var totalBytesRead = 0L;
var buffer = new byte[bufferSize];
while (true)
{
var buffer = new byte[bufferSize];
var bytesRead = await stream.ReadAsync(buffer);
if (bytesRead == 0) break;
await file.WriteAsync(buffer.AsMemory(0, bytesRead));
@ -59,22 +60,15 @@ public class DownloadService : IDownloadService
if (isIndeterminate)
{
OnDownloadProgressChanged(-1);
progress?.Report(new ProgressReport(-1, isIndeterminate: true));
}
else
{
var progress = totalBytesRead / (double) contentLength;
OnDownloadProgressChanged(progress);
progress?.Report(new ProgressReport(current: Convert.ToUInt64(totalBytesRead),
total: Convert.ToUInt64(contentLength)));
}
}
await file.FlushAsync();
OnDownloadComplete(downloadLocation);
}
private void OnDownloadProgressChanged(double progress) =>
DownloadProgressChanged?.Invoke(this, new ProgressReport(progress));
private void OnDownloadComplete(string path) =>
DownloadComplete?.Invoke(this, new ProgressReport(progress: 100f, message: path));
}

5
StabilityMatrix/Services/IDownloadService.cs

@ -6,7 +6,6 @@ namespace StabilityMatrix.Services;
public interface IDownloadService
{
event EventHandler<ProgressReport>? DownloadProgressChanged;
event EventHandler<ProgressReport>? DownloadComplete;
Task DownloadToFileAsync(string downloadUrl, string downloadLocation, int bufferSize = ushort.MaxValue);
Task DownloadToFileAsync(string downloadUrl, string downloadLocation, int bufferSize = ushort.MaxValue,
IProgress<ProgressReport>? progress = null);
}

4
StabilityMatrix/SettingsPage.xaml

@ -103,6 +103,10 @@
Command="{Binding PingWebApiCommand}"
Content="Ping Web API"
Margin="8" />
<Button
Command="{Binding DebugTriggerExceptionCommand}"
Content="Trigger Exception"
Margin="8" />
</StackPanel>
</StackPanel>
</ui:Card>

6
StabilityMatrix/StabilityMatrix.csproj

@ -8,6 +8,7 @@
<SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<ApplicationIcon>Assets\Icon.ico</ApplicationIcon>
<LangVersion>11</LangVersion>
</PropertyGroup>
<ItemGroup>
@ -81,6 +82,11 @@
<XamlRuntime>Wpf</XamlRuntime>
<SubType>Designer</SubType>
</Page>
<Page Update="CheckpointBrowserPage.xaml">
<Generator>MSBuild:Compile</Generator>
<XamlRuntime>Wpf</XamlRuntime>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

98
StabilityMatrix/ViewModels/CheckpointBrowserCardViewModel.cs

@ -0,0 +1,98 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Extensions;
using StabilityMatrix.Helper;
using StabilityMatrix.Models;
using StabilityMatrix.Models.Api;
using StabilityMatrix.Services;
namespace StabilityMatrix.ViewModels;
public partial class CheckpointBrowserCardViewModel : ProgressViewModel
{
private readonly IDownloadService downloadService;
private readonly ISnackbarService snackbarService;
public CivitModel CivitModel { get; init; }
public override Visibility ProgressVisibility => Value > 0 ? Visibility.Visible : Visibility.Collapsed;
public override Visibility TextVisibility => Value > 0 ? Visibility.Visible : Visibility.Collapsed;
public CheckpointBrowserCardViewModel(CivitModel civitModel, IDownloadService downloadService, ISnackbarService snackbarService)
{
this.downloadService = downloadService;
this.snackbarService = snackbarService;
CivitModel = civitModel;
}
[RelayCommand]
private void OpenModel()
{
Process.Start(new ProcessStartInfo
{
FileName = $"https://civitai.com/models/{CivitModel.Id}",
UseShellExecute = true
});
}
[RelayCommand]
private async Task Import(CivitModel model)
{
Text = "Downloading...";
var latestModelFile = model.ModelVersions[0].Files[0];
var fileExpectedSha256 = latestModelFile.Hashes.SHA256;
var downloadPath = Path.Combine(SharedFolders.SharedFoldersPath,
model.Type.ConvertTo<SharedFolderType>().GetStringValue(), latestModelFile.Name);
var downloadProgress = new Progress<ProgressReport>(progress =>
{
Value = progress.Percentage;
Text = $"Importing... {progress.Percentage}%";
});
await downloadService.DownloadToFileAsync(latestModelFile.DownloadUrl, downloadPath, progress: downloadProgress);
// When sha256 is available, validate the downloaded file
if (!string.IsNullOrEmpty(fileExpectedSha256))
{
var hashProgress = new Progress<ProgressReport>(progress =>
{
Value = progress.Percentage;
Text = $"Validating... {progress.Percentage}%";
});
var sha256 = await FileHash.GetSha256Async(downloadPath, hashProgress);
if (sha256 != fileExpectedSha256.ToLowerInvariant())
{
Text = "Import Failed!";
DelayedClearProgress(TimeSpan.FromSeconds(800));
await snackbarService.ShowSnackbarAsync(
"This may be caused by network or server issues from CivitAI, please try again in a few minutes.",
"Download failed hash validation", LogLevel.Warning);
return;
}
else
{
snackbarService.ShowSnackbarAsync($"{model.Type} {model.Name} imported successfully!",
"Import complete", LogLevel.Trace);
}
}
Text = "Import complete!";
Value = 100;
DelayedClearProgress(TimeSpan.FromMilliseconds(800));
}
private void DelayedClearProgress(TimeSpan delay)
{
Task.Delay(delay).ContinueWith(_ =>
{
Text = string.Empty;
Value = 0;
});
}
}

129
StabilityMatrix/ViewModels/CheckpointBrowserViewModel.cs

@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NLog;
using StabilityMatrix.Api;
using StabilityMatrix.Helper;
using StabilityMatrix.Models.Api;
using StabilityMatrix.Services;
namespace StabilityMatrix.ViewModels;
public partial class CheckpointBrowserViewModel : ObservableObject
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ICivitApi civitApi;
private readonly IDownloadService downloadService;
private readonly ISnackbarService snackbarService;
private const int MaxModelsPerPage = 14;
[ObservableProperty] private string? searchQuery;
[ObservableProperty] private ObservableCollection<CheckpointBrowserCardViewModel>? modelCards;
[ObservableProperty] private bool showNsfw;
[ObservableProperty] private bool showMainLoadingSpinner;
[ObservableProperty] private CivitPeriod selectedPeriod;
[ObservableProperty] private CivitSortMode sortMode;
[ObservableProperty] private int currentPageNumber;
[ObservableProperty] private int totalPages;
[ObservableProperty] private bool hasSearched;
[ObservableProperty] private bool canGoToNextPage;
[ObservableProperty] private bool canGoToPreviousPage;
[ObservableProperty] private bool isIndeterminate;
public IEnumerable<CivitPeriod> AllCivitPeriods => Enum.GetValues(typeof(CivitPeriod)).Cast<CivitPeriod>();
public IEnumerable<CivitSortMode> AllSortModes => Enum.GetValues(typeof(CivitSortMode)).Cast<CivitSortMode>();
public CheckpointBrowserViewModel(ICivitApi civitApi, IDownloadService downloadService, ISnackbarService snackbarService)
{
this.civitApi = civitApi;
this.downloadService = downloadService;
this.snackbarService = snackbarService;
SelectedPeriod = CivitPeriod.Month;
SortMode = CivitSortMode.HighestRated;
HasSearched = false;
CurrentPageNumber = 1;
CanGoToPreviousPage = false;
CanGoToNextPage = true;
}
[RelayCommand]
private async Task SearchModels()
{
if (string.IsNullOrWhiteSpace(SearchQuery))
{
return;
}
ShowMainLoadingSpinner = true;
var models = await civitApi.GetModels(new CivitModelsRequest
{
Query = SearchQuery,
Limit = MaxModelsPerPage,
Nsfw = ShowNsfw.ToString().ToLower(),
Sort = SortMode,
Period = SelectedPeriod,
Page = CurrentPageNumber
});
HasSearched = true;
TotalPages = models.Metadata.TotalPages;
CanGoToPreviousPage = CurrentPageNumber > 1;
CanGoToNextPage = CurrentPageNumber < TotalPages;
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(models.Items.Select(
m => new CheckpointBrowserCardViewModel(m, downloadService, snackbarService)));
ShowMainLoadingSpinner = false;
Logger.Debug($"Found {models.Items.Length} models");
}
[RelayCommand]
private async Task PreviousPage()
{
if (CurrentPageNumber == 1) return;
CurrentPageNumber--;
await TrySearchAgain(false);
}
[RelayCommand]
private async Task NextPage()
{
CurrentPageNumber++;
await TrySearchAgain(false);
}
partial void OnShowNsfwChanged(bool oldValue, bool newValue)
{
TrySearchAgain();
}
partial void OnSelectedPeriodChanged(CivitPeriod oldValue, CivitPeriod newValue)
{
TrySearchAgain();
}
partial void OnSortModeChanged(CivitSortMode oldValue, CivitSortMode newValue)
{
TrySearchAgain();
}
private async Task TrySearchAgain(bool shouldUpdatePageNumber = true)
{
if (!hasSearched) return;
ModelCards?.Clear();
if (shouldUpdatePageNumber)
{
CurrentPageNumber = 1;
}
// execute command instead of calling method directly so that the IsRunning property gets updated
await SearchModelsCommand.ExecuteAsync(null);
}
}

8
StabilityMatrix/ViewModels/CheckpointManagerViewModel.cs

@ -23,17 +23,17 @@ public partial class CheckpointManagerViewModel : ObservableObject
public async Task OnLoaded()
{
// Get all folders within the shared folder root
if (string.IsNullOrWhiteSpace(sharedFolders.SharedFoldersPath))
if (string.IsNullOrWhiteSpace(SharedFolders.SharedFoldersPath))
{
return;
}
// Skip if the shared folder root doesn't exist
if (!Directory.Exists(sharedFolders.SharedFoldersPath))
if (!Directory.Exists(SharedFolders.SharedFoldersPath))
{
Logger.Debug($"Skipped shared folder index - {sharedFolders.SharedFoldersPath} doesn't exist");
Logger.Debug($"Skipped shared folder index - {SharedFolders.SharedFoldersPath} doesn't exist");
return;
}
var folders = Directory.GetDirectories(sharedFolders.SharedFoldersPath);
var folders = Directory.GetDirectories(SharedFolders.SharedFoldersPath);
CheckpointFolders.Clear();

28
StabilityMatrix/ViewModels/ProgressViewModel.cs

@ -0,0 +1,28 @@
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
namespace StabilityMatrix.ViewModels;
/// <summary>
/// Generic view model for progress reporting.
/// </summary>
public partial class ProgressViewModel : ObservableObject
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(TextVisibility))]
private string text;
[ObservableProperty]
private double value;
[ObservableProperty]
private bool isIndeterminate;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ProgressVisibility))]
private bool isProgressVisible;
public virtual Visibility ProgressVisibility => IsProgressVisible ? Visibility.Visible : Visibility.Collapsed;
public virtual Visibility TextVisibility => string.IsNullOrEmpty(Text) ? Visibility.Collapsed : Visibility.Visible;
}

8
StabilityMatrix/ViewModels/SettingsViewModel.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Reflection;
@ -182,6 +183,13 @@ public partial class SettingsViewModel : ObservableObject
await dialog.ShowAsync();
}
[RelayCommand]
[DoesNotReturn]
private void DebugTriggerException()
{
throw new Exception("Test exception");
}
private void ApplyTheme(string value)
{
switch (value)

Loading…
Cancel
Save