Browse Source

Added ctrl+scroll for zoom to inference & outputs image browsers

pull/240/head
JT 1 year ago
parent
commit
4fa10cb27e
  1. 2
      CHANGELOG.md
  2. 3
      StabilityMatrix.Avalonia/App.axaml
  3. 23
      StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml
  4. 40
      StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs
  5. 14
      StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollContentPresenter.cs
  6. 69
      StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.axaml
  7. 5
      StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.cs
  8. 6
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml
  9. 37
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
  10. 54
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  11. 2
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  12. 2
      StabilityMatrix.Avalonia/Languages/Resources.resx
  13. 6
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  14. 2
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  15. 13
      StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs
  16. 12
      StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
  17. 18
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
  18. 38
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  19. 14
      StabilityMatrix.Avalonia/Views/OutputsPage.axaml
  20. 31
      StabilityMatrix.Avalonia/Views/OutputsPage.axaml.cs
  21. 3
      StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml
  22. 6
      StabilityMatrix.Core/Models/Settings/Settings.cs
  23. 10
      StabilityMatrix.Core/Models/Settings/Size.cs
  24. 3
      StabilityMatrix.Core/Services/ISettingsManager.cs
  25. 43
      StabilityMatrix.Core/Services/SettingsManager.cs

2
CHANGELOG.md

@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
- Added filtering of "incompatible" packages (ones that do not support your GPU) to all installers - Added filtering of "incompatible" packages (ones that do not support your GPU) to all installers
- This can be overridden by checking the new "Show All Packages" checkbox - This can be overridden by checking the new "Show All Packages" checkbox
- Added more launch options for Fooocus, such as the `--preset` option - Added more launch options for Fooocus, such as the `--preset` option
- Added Ctrl+ScrollWheel to change image size in the inference output gallery and new Outputs page
- Added "No Images Found" placeholder for non-connected models on the Checkpoints tab
### Changed ### Changed
- If ComfyUI for Inference is chosen during the One-Click Installer, the Inference page will be opened after installation instead of the Launch page - If ComfyUI for Inference is chosen during the One-Click Installer, the Inference page will be opened after installation instead of the Launch page
- Changed all package installs & updates to use git commands instead of downloading zip files - Changed all package installs & updates to use git commands instead of downloading zip files

3
StabilityMatrix.Avalonia/App.axaml

@ -20,6 +20,8 @@
<ResourceInclude Source="Controls/CodeCompletion/CompletionListThemes.axaml"/> <ResourceInclude Source="Controls/CodeCompletion/CompletionListThemes.axaml"/>
<ResourceInclude Source="Styles/ContextMenuStyles.axaml"/> <ResourceInclude Source="Styles/ContextMenuStyles.axaml"/>
<ResourceInclude Source="Controls/EditorFlyouts.axaml"/> <ResourceInclude Source="Controls/EditorFlyouts.axaml"/>
<ResourceInclude Source="Controls/Scroll/BetterScrollViewer.axaml"/>
<ResourceInclude Source="Controls/ImageFolderCard.axaml"/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<idcr:ControlRecycling x:Key="ControlRecyclingKey" /> <idcr:ControlRecycling x:Key="ControlRecyclingKey" />
@ -56,7 +58,6 @@
<StyleInclude Source="Controls/FrameCarousel.axaml"/> <StyleInclude Source="Controls/FrameCarousel.axaml"/>
<StyleInclude Source="Controls/CodeCompletion/CompletionWindow.axaml"/> <StyleInclude Source="Controls/CodeCompletion/CompletionWindow.axaml"/>
<StyleInclude Source="Controls/SelectImageCard.axaml"/> <StyleInclude Source="Controls/SelectImageCard.axaml"/>
<StyleInclude Source="Controls/ImageFolderCard.axaml"/>
<StyleInclude Source="Controls/SharpenCard.axaml"/> <StyleInclude Source="Controls/SharpenCard.axaml"/>
<StyleInclude Source="Controls/FreeUCard.axaml"/> <StyleInclude Source="Controls/FreeUCard.axaml"/>
<StyleInclude Source="Controls/Paginator.axaml"/> <StyleInclude Source="Controls/Paginator.axaml"/>

23
StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml

@ -1,4 +1,4 @@
<Styles <ResourceDictionary
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls" xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
@ -8,6 +8,7 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:scroll="clr-namespace:StabilityMatrix.Avalonia.Controls.Scroll"
x:DataType="vmInference:ImageFolderCardViewModel"> x:DataType="vmInference:ImageFolderCardViewModel">
<Design.PreviewWith> <Design.PreviewWith>
@ -18,7 +19,8 @@
</Panel> </Panel>
</Design.PreviewWith> </Design.PreviewWith>
<Style Selector="controls|ImageFolderCard"> <ControlTheme x:Key="{x:Type controls:ImageFolderCard}" TargetType="controls:ImageFolderCard">
<Setter Property="Background" Value="Transparent" />
<!-- Set Defaults --> <!-- Set Defaults -->
<Setter Property="CornerRadius" Value="8" /> <Setter Property="CornerRadius" Value="8" />
<Setter Property="Focusable" Value="True" /> <Setter Property="Focusable" Value="True" />
@ -54,13 +56,15 @@
</TextBox.InnerRightContent> </TextBox.InnerRightContent>
</TextBox> </TextBox>
<ScrollViewer <scroll:BetterScrollViewer
Grid.Row="1" Grid.Row="1"
BringIntoViewOnFocusChange="False" BringIntoViewOnFocusChange="False"
HorizontalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Disabled"
IsScrollInertiaEnabled="True" IsScrollInertiaEnabled="True"
VerticalSnapPointsType="Mandatory"> VerticalSnapPointsType="Mandatory">
<ItemsRepeater <ItemsRepeater
Name="ImageRepeater"
x:Name="ImageRepeater"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{Binding LocalImages}" ItemsSource="{Binding LocalImages}"
@ -198,8 +202,8 @@
<Border ClipToBounds="True" CornerRadius="8"> <Border ClipToBounds="True" CornerRadius="8">
<Grid RowDefinitions="*,Auto"> <Grid RowDefinitions="*,Auto">
<controls:BetterAdvancedImage <controls:BetterAdvancedImage
Width="150" Width="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Width}"
Height="190" Height="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Height}"
Source="{Binding AbsolutePath}" Source="{Binding AbsolutePath}"
Stretch="UniformToFill" Stretch="UniformToFill"
StretchDirection="Both" /> StretchDirection="Both" />
@ -207,7 +211,7 @@
<Border <Border
Grid.Row="1" Grid.Row="1"
MinHeight="20" MinHeight="20"
MaxWidth="150" Width="{Binding $parent[ItemsRepeater].((vmInference:ImageFolderCardViewModel)DataContext).ImageSize.Width}"
Padding="4,0,0,0" Padding="4,0,0,0"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Classes="theme-dark" Classes="theme-dark"
@ -216,6 +220,7 @@
VerticalAlignment="Center" VerticalAlignment="Center"
FontSize="12" FontSize="12"
Text="{Binding FileNameWithoutExtension}" Text="{Binding FileNameWithoutExtension}"
TextAlignment="Center"
TextTrimming="CharacterEllipsis" /> TextTrimming="CharacterEllipsis" />
</Border> </Border>
</Grid> </Grid>
@ -224,10 +229,10 @@
</DataTemplate> </DataTemplate>
</ItemsRepeater.ItemTemplate> </ItemsRepeater.ItemTemplate>
</ItemsRepeater> </ItemsRepeater>
</ScrollViewer> </scroll:BetterScrollViewer>
</Grid> </Grid>
</controls:Card> </controls:Card>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </ControlTheme>
</Styles> </ResourceDictionary>

40
StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs

@ -1,11 +1,23 @@
using Avalonia.Input; using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Settings;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;
[Transient] [Transient]
public class ImageFolderCard : DropTargetTemplatedControlBase public class ImageFolderCard : DropTargetTemplatedControlBase
{ {
private ItemsRepeater? imageRepeater;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
imageRepeater = e.NameScope.Find<ItemsRepeater>("ImageRepeater");
base.OnApplyTemplate(e);
}
/// <inheritdoc /> /// <inheritdoc />
protected override void DropHandler(object? sender, DragEventArgs e) protected override void DropHandler(object? sender, DragEventArgs e)
{ {
@ -19,4 +31,30 @@ public class ImageFolderCard : DropTargetTemplatedControlBase
base.DragOverHandler(sender, e); base.DragOverHandler(sender, e);
e.Handled = true; e.Handled = true;
} }
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
if (e.KeyModifiers != KeyModifiers.Control)
return;
if (DataContext is not ImageFolderCardViewModel vm)
return;
if (e.Delta.Y > 0)
{
if (vm.ImageSize.Height >= 500)
return;
vm.ImageSize += new Size(15, 19);
}
else
{
if (vm.ImageSize.Height <= 200)
return;
vm.ImageSize -= new Size(15, 19);
}
imageRepeater?.InvalidateArrange();
imageRepeater?.InvalidateMeasure();
e.Handled = true;
}
} }

14
StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollContentPresenter.cs

@ -0,0 +1,14 @@
using Avalonia.Controls.Presenters;
using Avalonia.Input;
namespace StabilityMatrix.Avalonia.Controls.Scroll;
public class BetterScrollContentPresenter : ScrollContentPresenter
{
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
if (e.KeyModifiers == KeyModifiers.Control)
return;
base.OnPointerWheelChanged(e);
}
}

69
StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.axaml

@ -0,0 +1,69 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scroll="clr-namespace:StabilityMatrix.Avalonia.Controls.Scroll">
<Design.PreviewWith>
<scroll:BetterScrollViewer Width="200" Height="200"
HorizontalScrollBarVisibility="Auto">
<StackPanel Spacing="20" Width="210">
<TextBlock>Item 1</TextBlock>
<TextBlock>Item 2</TextBlock>
<TextBlock>Item 3</TextBlock>
<TextBlock>Item 4</TextBlock>
<TextBlock>Item 5</TextBlock>
<TextBlock>Item 6</TextBlock>
<TextBlock>Item 7</TextBlock>
<TextBlock>Item 8</TextBlock>
<TextBlock>Item 9</TextBlock>
</StackPanel>
</scroll:BetterScrollViewer>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type scroll:BetterScrollViewer}" TargetType="scroll:BetterScrollViewer">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
<scroll:BetterScrollContentPresenter x:Name="PART_ContentPresenter"
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2"
Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}"
VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}"
HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}"
VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}"
Padding="{TemplateBinding Padding}"
ScrollViewer.IsScrollInertiaEnabled="{TemplateBinding IsScrollInertiaEnabled}">
<scroll:BetterScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer
CanHorizontallyScroll="{Binding CanHorizontallyScroll, ElementName=PART_ContentPresenter}"
CanVerticallyScroll="{Binding CanVerticallyScroll, ElementName=PART_ContentPresenter}"
IsScrollInertiaEnabled="{Binding (ScrollViewer.IsScrollInertiaEnabled), ElementName=PART_ContentPresenter}" />
</scroll:BetterScrollContentPresenter.GestureRecognizers>
</scroll:BetterScrollContentPresenter>
<ScrollBar Name="PART_HorizontalScrollBar"
Orientation="Horizontal"
Grid.Row="1" />
<ScrollBar Name="PART_VerticalScrollBar"
Orientation="Vertical"
Grid.Column="1" />
<Panel x:Name="PART_ScrollBarsSeparator"
Grid.Row="1"
Grid.Column="1"
Background="{DynamicResource ScrollViewerScrollBarsSeparatorBackground}"
Opacity="0">
<Panel.Transitions>
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.1" />
</Transitions>
</Panel.Transitions>
</Panel>
</Grid>
</ControlTemplate>
</Setter>
<Style Selector="^[IsExpanded=true] /template/ Panel#PART_ScrollBarsSeparator">
<Setter Property="Opacity" Value="1" />
</Style>
</ControlTheme>
</ResourceDictionary>

5
StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.cs

@ -0,0 +1,5 @@
using Avalonia.Controls;
namespace StabilityMatrix.Avalonia.Controls.Scroll;
public class BetterScrollViewer : ScrollViewer { }

6
StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml

@ -14,7 +14,9 @@
<Style Selector="selectableImageCard|SelectableImageButton"> <Style Selector="selectableImageCard|SelectableImageButton">
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Grid> <Grid
Width="{Binding ImageWidth, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
Height="{Binding ImageHeight, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<CheckBox VerticalAlignment="Top" <CheckBox VerticalAlignment="Top"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Margin="14,8" Margin="14,8"
@ -37,8 +39,6 @@
Command="{TemplateBinding Command}" Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}"> CommandParameter="{TemplateBinding CommandParameter}">
<controls:BetterAdvancedImage <controls:BetterAdvancedImage
Width="300"
Height="300"
Stretch="UniformToFill" Stretch="UniformToFill"
CornerRadius="8" CornerRadius="8"
ContextFlyout="{TemplateBinding ContextFlyout}" ContextFlyout="{TemplateBinding ContextFlyout}"

37
StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs

@ -1,15 +1,46 @@
using Avalonia; using System;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives;
namespace StabilityMatrix.Avalonia.Controls.SelectableImageCard; namespace StabilityMatrix.Avalonia.Controls.SelectableImageCard;
public class SelectableImageButton : Button public class SelectableImageButton : Button
{ {
public static readonly StyledProperty<bool?> IsSelectedProperty = public static readonly StyledProperty<bool?> IsSelectedProperty =
CheckBox.IsCheckedProperty.AddOwner<SelectableImageButton>(); ToggleButton.IsCheckedProperty.AddOwner<SelectableImageButton>();
public static readonly StyledProperty<string?> SourceProperty = public static readonly StyledProperty<string?> SourceProperty =
BetterAdvancedImage.SourceProperty.AddOwner<SelectableImageButton>(); AdvancedImage.SourceProperty.AddOwner<SelectableImageButton>();
public static readonly StyledProperty<double> ImageWidthProperty = AvaloniaProperty.Register<
SelectableImageButton,
double
>("ImageWidth", 300);
public static readonly StyledProperty<double> ImageHeightProperty = AvaloniaProperty.Register<
SelectableImageButton,
double
>("ImageHeight", 300);
static SelectableImageButton()
{
AffectsRender<SelectableImageButton>(ImageWidthProperty, ImageHeightProperty);
AffectsArrange<SelectableImageButton>(ImageWidthProperty, ImageHeightProperty);
}
public double ImageHeight
{
get => GetValue(ImageHeightProperty);
set => SetValue(ImageHeightProperty, value);
}
public double ImageWidth
{
get => GetValue(ImageWidthProperty);
set => SetValue(ImageWidthProperty, value);
}
public bool? IsSelected public bool? IsSelected
{ {

54
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -6,6 +6,7 @@ using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using AvaloniaEdit.Utils; using AvaloniaEdit.Utils;
using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.Controls.CodeCompletion;
@ -173,6 +174,55 @@ public static class DesignData
InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0]; InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0];
InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note."; InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note.";
ObservableCacheEx.AddOrUpdate(
CheckpointsPageViewModel.CheckpointFoldersCache,
new CheckpointFolder[]
{
new(settingsManager, downloadService, modelFinder, notificationService)
{
DirectoryPath = "Models/StableDiffusion",
DisplayedCheckpointFiles = new ObservableCollectionExtended<CheckpointFile>()
{
new()
{
FilePath = "~/Models/StableDiffusion/electricity-light.safetensors",
Title = "Auroral Background",
PreviewImagePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/"
+ "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
ConnectedModel = new ConnectedModelInfo
{
VersionName = "Lightning Auroral",
BaseModel = "SD 1.5",
ModelName = "Auroral Background",
ModelType = CivitModelType.Model,
FileMetadata = new CivitFileMetadata
{
Format = CivitModelFormat.SafeTensor,
Fp = CivitModelFpType.fp16,
Size = CivitModelSize.pruned,
}
}
},
new()
{
FilePath = "~/Models/Lora/model.safetensors",
Title = "Some model"
},
},
},
new(settingsManager, downloadService, modelFinder, notificationService)
{
Title = "Lora",
DirectoryPath = "Packages/Lora",
DisplayedCheckpointFiles = new ObservableCollectionExtended<CheckpointFile>
{
new() { FilePath = "~/Models/Lora/lora_v2.pt", Title = "Best Lora v2", }
}
}
}
);
/*// Checkpoints page /*// Checkpoints page
CheckpointsPageViewModel.CheckpointFolders = CheckpointsPageViewModel.CheckpointFolders =
new CheckpointFolder[] new CheckpointFolder[]
@ -618,8 +668,8 @@ The gallery images are often inpainted, but you will get something very similar
get get
{ {
var list = new CompletionList { IsFiltering = true }; var list = new CompletionList { IsFiltering = true };
list.CompletionData.AddRange(SampleCompletionData); ExtensionMethods.AddRange(list.CompletionData, SampleCompletionData);
list.FilteredCompletionData.AddRange(list.CompletionData); ExtensionMethods.AddRange(list.FilteredCompletionData, list.CompletionData);
list.SelectItem("te", true); list.SelectItem("te", true);
return list; return list;
} }

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

@ -942,7 +942,7 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Import as Connected. /// Looks up a localized string similar to Import with Metadata.
/// </summary> /// </summary>
public static string Label_ImportAsConnected { public static string Label_ImportAsConnected {
get { get {

2
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -379,7 +379,7 @@
<value>Drop file here to import</value> <value>Drop file here to import</value>
</data> </data>
<data name="Label_ImportAsConnected" xml:space="preserve"> <data name="Label_ImportAsConnected" xml:space="preserve">
<value>Import as Connected</value> <value>Import with Metadata</value>
</data> </data>
<data name="Label_ImportAsConnectedExplanation" xml:space="preserve"> <data name="Label_ImportAsConnectedExplanation" xml:space="preserve">
<value>Search for connected metadata on new local imports</value> <value>Search for connected metadata on new local imports</value>

6
StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs

@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using NLog; using NLog;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
@ -260,6 +261,11 @@ public partial class CheckpointFile : ViewModelBase
.Where(File.Exists) .Where(File.Exists)
.FirstOrDefault(); .FirstOrDefault();
if (string.IsNullOrWhiteSpace(checkpointFile.PreviewImagePath))
{
checkpointFile.PreviewImagePath = Assets.NoImage.ToString();
}
yield return checkpointFile; yield return checkpointFile;
} }
} }

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

@ -102,7 +102,7 @@ public partial class CheckpointFolder : ViewModelBase
public IObservableCollection<CheckpointFile> CheckpointFiles { get; } = public IObservableCollection<CheckpointFile> CheckpointFiles { get; } =
new ObservableCollectionExtended<CheckpointFile>(); new ObservableCollectionExtended<CheckpointFile>();
public IObservableCollection<CheckpointFile> DisplayedCheckpointFiles { get; } = public IObservableCollection<CheckpointFile> DisplayedCheckpointFiles { get; set; } =
new ObservableCollectionExtended<CheckpointFile>(); new ObservableCollectionExtended<CheckpointFile>();
public CheckpointFolder( public CheckpointFolder(

13
StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs

@ -25,6 +25,7 @@ using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using SortDirection = DynamicData.Binding.SortDirection; using SortDirection = DynamicData.Binding.SortDirection;
@ -44,6 +45,9 @@ public partial class ImageFolderCardViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private string? searchQuery; private string? searchQuery;
[ObservableProperty]
private Size imageSize = new(150, 190);
/// <summary> /// <summary>
/// Collection of local image files /// Collection of local image files
/// </summary> /// </summary>
@ -76,6 +80,13 @@ public partial class ImageFolderCardViewModel : ViewModelBase
.SortBy(file => file.LastModifiedAt, SortDirection.Descending) .SortBy(file => file.LastModifiedAt, SortDirection.Descending)
.Bind(LocalImages) .Bind(LocalImages)
.Subscribe(); .Subscribe();
settingsManager.RelayPropertyFor(
this,
vm => vm.ImageSize,
settings => settings.InferenceImageSize,
delay: TimeSpan.FromMilliseconds(250)
);
} }
private static bool SearchPredicate(LocalImageFile file, string? query) private static bool SearchPredicate(LocalImageFile file, string? query)
@ -117,7 +128,7 @@ public partial class ImageFolderCardViewModel : ViewModelBase
public override async Task OnLoadedAsync() public override async Task OnLoadedAsync()
{ {
await base.OnLoadedAsync(); await base.OnLoadedAsync();
ImageSize = settingsManager.Settings.InferenceImageSize;
imageIndexService.RefreshIndexForAllCollections().SafeFireAndForget(); imageIndexService.RefreshIndexForAllCollections().SafeFireAndForget();
} }

12
StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs

@ -31,6 +31,7 @@ using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Size = StabilityMatrix.Core.Models.Settings.Size;
using Symbol = FluentIcons.Common.Symbol; using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -75,6 +76,9 @@ public partial class OutputsPageViewModel : PageViewModelBase
[ObservableProperty] [ObservableProperty]
private string searchQuery; private string searchQuery;
[ObservableProperty]
private Size imageSize = new(300, 300);
public bool CanShowOutputTypes => public bool CanShowOutputTypes =>
SelectedCategory?.Name?.Equals("Shared Output Folder") ?? false; SelectedCategory?.Name?.Equals("Shared Output Folder") ?? false;
@ -132,6 +136,13 @@ public partial class OutputsPageViewModel : PageViewModelBase
{ {
NumItemsSelected = Outputs.Count(o => o.IsSelected); NumItemsSelected = Outputs.Count(o => o.IsSelected);
}); });
settingsManager.RelayPropertyFor(
this,
vm => vm.ImageSize,
settings => settings.OutputsImageSize,
delay: TimeSpan.FromMilliseconds(250)
);
} }
public override void OnLoaded() public override void OnLoaded()
@ -172,6 +183,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
SelectedCategory = Categories.First(); SelectedCategory = Categories.First();
SelectedOutputType = SharedOutputType.All; SelectedOutputType = SharedOutputType.All;
SearchQuery = string.Empty; SearchQuery = string.Empty;
ImageSize = settingsManager.Settings.OutputsImageSize;
var path = var path =
CanShowOutputTypes && SelectedOutputType != SharedOutputType.All CanShowOutputTypes && SelectedOutputType != SharedOutputType.All

18
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml

@ -21,7 +21,7 @@
<controls:Card <controls:Card
Margin="8" Margin="8"
MaxHeight="450" MaxHeight="450"
CornerRadius="8" CornerRadius="12"
Name="ModelCard" Name="ModelCard"
Width="330"> Width="330">
@ -66,6 +66,7 @@
<controls:BetterAdvancedImage <controls:BetterAdvancedImage
Margin="0,8,0,8" Margin="0,8,0,8"
Height="300" Height="300"
Width="300"
StretchDirection="Both" StretchDirection="Both"
CornerRadius="8" CornerRadius="8"
VerticalContentAlignment="Top" VerticalContentAlignment="Top"
@ -150,6 +151,8 @@
</StackPanel> </StackPanel>
<Rectangle <Rectangle
Fill="#DD000000" Fill="#DD000000"
RadiusX="8"
RadiusY="8"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Margin="0,8,0,8" Margin="0,8,0,8"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
@ -177,16 +180,17 @@
</Grid> </Grid>
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Button <Button
Classes="accent" Classes="accent"
Margin="0,8,0,0"
Command="{Binding ImportCommand}" Command="{Binding ImportCommand}"
CommandParameter="{Binding CivitModel}" CommandParameter="{Binding CivitModel}"
IsEnabled="{Binding !IsImporting}" IsEnabled="{Binding !IsImporting}"
HorizontalAlignment="Stretch" CornerRadius="6"
Margin="0,8,0,0"> HorizontalAlignment="Stretch">
<Button.Content> <Button.Content>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Static lang:Resources.Label_ImportLatest}" /> <TextBlock Text="{x:Static lang:Resources.Label_ImportLatest}" />
@ -197,6 +201,7 @@
</Button> </Button>
<Button Grid.Column="1" <Button Grid.Column="1"
CornerRadius="6"
Margin="8,8,0,0" Margin="8,8,0,0"
Classes="accent" Classes="accent"
IsEnabled="{Binding !IsImporting}" IsEnabled="{Binding !IsImporting}"
@ -244,7 +249,7 @@
</Button> </Button>
</Grid> </Grid>
<DockPanel> <DockPanel>
<StackPanel Margin="4, 8" Orientation="Vertical"> <StackPanel Margin="8, 8,4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_Sort}" /> <Label Content="{x:Static lang:Resources.Label_Sort}" />
<ComboBox <ComboBox
ItemsSource="{Binding AllSortModes}" ItemsSource="{Binding AllSortModes}"
@ -286,7 +291,8 @@
</StackPanel> </StackPanel>
<ScrollViewer Grid.Row="1"> <ScrollViewer Grid.Row="1"
Margin="8,0,8,0">
<ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}" <ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}"
ItemsSource="{Binding ModelCards}"> ItemsSource="{Binding ModelCards}">
<ItemsRepeater.Layout> <ItemsRepeater.Layout>

38
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -30,7 +30,7 @@
Height="18" Height="18"
Margin="4,0,0,0" Margin="4,0,0,0"
Padding="3" Padding="3"
Width="40"> Width="48">
<TextBlock <TextBlock
FontSize="10" FontSize="10"
FontWeight="Medium" FontWeight="Medium"
@ -47,9 +47,10 @@
DragDrop.AllowDrop="False" DragDrop.AllowDrop="False"
Background="Transparent" Background="Transparent"
BorderThickness="2" BorderThickness="2"
Margin="8"> Margin="4">
<controls:Card Width="260"> <controls:Card MaxWidth="330" Width="330"
CornerRadius="12">
<!-- Right click menu for a checkpoint file --> <!-- Right click menu for a checkpoint file -->
<controls:Card.ContextFlyout> <controls:Card.ContextFlyout>
<ui:FAMenuFlyout> <ui:FAMenuFlyout>
@ -65,9 +66,9 @@
<Grid> <Grid>
<!-- Main contents, hidden when IsLoading is true --> <!-- Main contents, hidden when IsLoading is true -->
<StackPanel MinHeight="70"> <StackPanel MinHeight="70">
<Grid ColumnDefinitions="*,*,*,*,*" RowDefinitions="*,Auto,0.1*" IsVisible="{Binding !IsLoading}"> <Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto,*,Auto" IsVisible="{Binding !IsLoading}">
<StackPanel <StackPanel
Grid.ColumnSpan="4" Grid.ColumnSpan="2"
HorizontalAlignment="Left" HorizontalAlignment="Left"
IsEnabled="True" IsEnabled="True"
Margin="0,0,0,4" Margin="0,0,0,4"
@ -121,15 +122,17 @@
</StackPanel> </StackPanel>
<controls:BetterAdvancedImage Grid.Row="1" <controls:BetterAdvancedImage Grid.Row="1"
Grid.Column="0" Grid.ColumnSpan="5" Grid.Column="0"
Margin="0,0,0,4" Grid.ColumnSpan="2"
CornerRadius="4" Margin="0, 4,0,4"
CornerRadius="8"
Source="{Binding PreviewImagePath}" Source="{Binding PreviewImagePath}"
Stretch="UniformToFill" Stretch="UniformToFill"
MaxHeight="250" Height="300"
Width="300"
IsVisible="{Binding $parent[ItemsControl].((vm:CheckpointsPageViewModel)DataContext).ShowConnectedModelImages}" /> IsVisible="{Binding $parent[ItemsControl].((vm:CheckpointsPageViewModel)DataContext).ShowConnectedModelImages}" />
<Grid Grid.Column="4" Grid.Row="0"> <Grid Grid.Column="1" Grid.Row="0">
<Button <Button
Background="Transparent" Background="Transparent"
BorderBrush="Transparent" BorderBrush="Transparent"
@ -169,7 +172,6 @@
FontSize="11" FontSize="11"
Foreground="{DynamicResource TextFillColorTertiaryBrush}" Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="2" Grid.Row="2"
IsEnabled="True" IsEnabled="True"
Margin="4,4" Margin="4,4"
@ -178,15 +180,15 @@
ToolTip.Tip="{Binding FileName}" ToolTip.Tip="{Binding FileName}"
VerticalAlignment="Bottom" /> VerticalAlignment="Bottom" />
<ItemsRepeater <ItemsRepeater
Grid.Column="3" Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="2" Grid.Row="2"
HorizontalAlignment="Right"
ItemTemplate="{StaticResource BadgeTemplate}" ItemTemplate="{StaticResource BadgeTemplate}"
ItemsSource="{Binding Badges}" ItemsSource="{Binding Badges}"
VerticalAlignment="Bottom"> VerticalAlignment="Bottom">
<ItemsRepeater.Layout> <ItemsRepeater.Layout>
<UniformGridLayout/> <UniformGridLayout />
</ItemsRepeater.Layout> </ItemsRepeater.Layout>
</ItemsRepeater> </ItemsRepeater>
</Grid> </Grid>
@ -213,8 +215,9 @@
<!-- Checkpoint Folder Expander --> <!-- Checkpoint Folder Expander -->
<DataTemplate DataType="{x:Type checkpointManager:CheckpointFolder}" x:Key="CheckpointFolderGridDataTemplate"> <DataTemplate DataType="{x:Type checkpointManager:CheckpointFolder}" x:Key="CheckpointFolderGridDataTemplate">
<Expander <Expander
CornerRadius="8"
IsExpanded="{Binding IsExpanded}" IsExpanded="{Binding IsExpanded}"
Margin="8" Margin="4"
Padding="8,8,8,8" Padding="8,8,8,8"
IsVisible="{Binding IsCategoryEnabled, FallbackValue=True}"> IsVisible="{Binding IsCategoryEnabled, FallbackValue=True}">
@ -268,7 +271,8 @@
ItemTemplate="{StaticResource CheckpointFileDataTemplate}" ItemTemplate="{StaticResource CheckpointFileDataTemplate}"
ItemsSource="{Binding DisplayedCheckpointFiles}"> ItemsSource="{Binding DisplayedCheckpointFiles}">
<ItemsRepeater.Layout> <ItemsRepeater.Layout>
<UniformGridLayout Orientation="Horizontal"/> <UniformGridLayout Orientation="Horizontal"
MinColumnSpacing="4" MinRowSpacing="4"/>
</ItemsRepeater.Layout> </ItemsRepeater.Layout>
<TextBlock Text="Hi"/> <TextBlock Text="Hi"/>
</ItemsRepeater> </ItemsRepeater>
@ -280,7 +284,7 @@
IsVisible="{Binding !CheckpointFiles.Count}"/> IsVisible="{Binding !CheckpointFiles.Count}"/>
<!-- Blurred background for drag and drop --> <!-- Blurred background for drag and drop -->
<Border <Border
CornerRadius="8" CornerRadius="4"
Grid.RowSpan="4" Grid.RowSpan="4"
IsEnabled="False" IsEnabled="False"
IsVisible="{Binding IsDragBlurEnabled}"> IsVisible="{Binding IsDragBlurEnabled}">

14
StabilityMatrix.Avalonia/Views/OutputsPage.axaml

@ -15,6 +15,7 @@
xmlns:outputsPage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.OutputsPage" xmlns:outputsPage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.OutputsPage"
xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard" xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:scroll="clr-namespace:StabilityMatrix.Avalonia.Controls.Scroll"
d:DataContext="{x:Static mocks:DesignData.OutputsPageViewModel}" d:DataContext="{x:Static mocks:DesignData.OutputsPageViewModel}"
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="700" d:DesignWidth="700"
@ -32,7 +33,6 @@
Margin="4" /> Margin="4" />
<ComboBox Grid.Column="0" <ComboBox Grid.Column="0"
Grid.Row="1" ItemsSource="{Binding Categories}" Grid.Row="1" ItemsSource="{Binding Categories}"
CornerRadius="8"
Margin="4,0" Margin="4,0"
SelectedItem="{Binding SelectedCategory}" SelectedItem="{Binding SelectedCategory}"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
@ -62,7 +62,6 @@
IsVisible="{Binding CanShowOutputTypes}" /> IsVisible="{Binding CanShowOutputTypes}" />
<ComboBox Grid.Column="1" Grid.Row="1" ItemsSource="{Binding OutputTypes}" <ComboBox Grid.Column="1" Grid.Row="1" ItemsSource="{Binding OutputTypes}"
IsVisible="{Binding CanShowOutputTypes}" IsVisible="{Binding CanShowOutputTypes}"
CornerRadius="8"
SelectedItem="{Binding SelectedOutputType}" SelectedItem="{Binding SelectedOutputType}"
MinWidth="150" MinWidth="150"
Margin="4,0" Margin="4,0"
@ -94,7 +93,6 @@
<Button Grid.Row="1" <Button Grid.Row="1"
Grid.Column="5" Grid.Column="5"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
CornerRadius="8"
Padding="12, 0" Padding="12, 0"
Height="40" Height="40"
Classes="danger" Classes="danger"
@ -107,7 +105,6 @@
Grid.Column="6" Grid.Column="6"
Content="{x:Static lang:Resources.Action_ClearSelection}" Content="{x:Static lang:Resources.Action_ClearSelection}"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
CornerRadius="8"
Margin="8, 0" Margin="8, 0"
Height="40" Height="40"
Command="{Binding ClearSelection}" Command="{Binding ClearSelection}"
@ -117,15 +114,16 @@
Content="{x:Static lang:Resources.Action_SelectAll}" Content="{x:Static lang:Resources.Action_SelectAll}"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Classes="accent" Classes="accent"
CornerRadius="8"
Margin="8, 0" Margin="8, 0"
Height="40" Height="40"
Command="{Binding SelectAll}" Command="{Binding SelectAll}"
IsVisible="{Binding !NumItemsSelected}" /> IsVisible="{Binding !NumItemsSelected}" />
</Grid> </Grid>
<ScrollViewer Grid.Row="1"> <scroll:BetterScrollViewer Grid.Row="1"
PointerWheelChanged="ScrollViewer_MouseWheelChanged">
<ItemsRepeater <ItemsRepeater
x:Name="ImageRepeater"
ItemsSource="{Binding Outputs}" ItemsSource="{Binding Outputs}"
VerticalAlignment="Top"> VerticalAlignment="Top">
<ItemsRepeater.Layout> <ItemsRepeater.Layout>
@ -134,6 +132,8 @@
<ItemsRepeater.ItemTemplate> <ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type outputsPage:OutputImageViewModel}"> <DataTemplate DataType="{x:Type outputsPage:OutputImageViewModel}">
<selectableImageCard:SelectableImageButton <selectableImageCard:SelectableImageButton
ImageHeight="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).ImageSize.Height}"
ImageWidth="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).ImageSize.Width}"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).OnImageClick}" Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).OnImageClick}"
CommandParameter="{Binding }" CommandParameter="{Binding }"
IsSelected="{Binding IsSelected}" IsSelected="{Binding IsSelected}"
@ -208,6 +208,6 @@
</DataTemplate> </DataTemplate>
</ItemsRepeater.ItemTemplate> </ItemsRepeater.ItemTemplate>
</ItemsRepeater> </ItemsRepeater>
</ScrollViewer> </scroll:BetterScrollViewer>
</Grid> </Grid>
</controls:UserControlBase> </controls:UserControlBase>

31
StabilityMatrix.Avalonia/Views/OutputsPage.axaml.cs

@ -1,5 +1,8 @@
using StabilityMatrix.Avalonia.Controls; using Avalonia.Input;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Settings;
namespace StabilityMatrix.Avalonia.Views; namespace StabilityMatrix.Avalonia.Views;
@ -10,4 +13,30 @@ public partial class OutputsPage : UserControlBase
{ {
InitializeComponent(); InitializeComponent();
} }
private void ScrollViewer_MouseWheelChanged(object? sender, PointerWheelEventArgs e)
{
if (e.KeyModifiers != KeyModifiers.Control)
return;
if (DataContext is not OutputsPageViewModel vm)
return;
if (e.Delta.Y > 0)
{
if (vm.ImageSize.Height >= 500)
return;
vm.ImageSize += new Size(10, 10);
}
else
{
if (vm.ImageSize.Height <= 200)
return;
vm.ImageSize -= new Size(10, 10);
}
ImageRepeater.InvalidateArrange();
ImageRepeater.InvalidateMeasure();
e.Handled = true;
}
} }

3
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml

@ -31,7 +31,8 @@
</ItemsRepeater.Layout> </ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate> <ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type packageManager:PackageCardViewModel}"> <DataTemplate DataType="{x:Type packageManager:PackageCardViewModel}">
<controls:Card Padding="8"> <controls:Card Padding="8"
CornerRadius="8">
<Grid RowDefinitions="Auto, Auto, Auto, Auto" <Grid RowDefinitions="Auto, Auto, Auto, Auto"
ColumnDefinitions="*,Auto"> ColumnDefinitions="*,Auto">

6
StabilityMatrix.Core/Models/Settings/Settings.cs

@ -1,4 +1,5 @@
using System.Globalization; using System.Drawing;
using System.Globalization;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Semver; using Semver;
using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Converters.Json;
@ -93,6 +94,9 @@ public class Settings
public HashSet<int> FavoriteModels { get; set; } = new(); public HashSet<int> FavoriteModels { get; set; } = new();
public Size InferenceImageSize { get; set; } = new(150, 190);
public Size OutputsImageSize { get; set; } = new(300, 300);
public void RemoveInstalledPackageAndUpdateActive(InstalledPackage package) public void RemoveInstalledPackageAndUpdateActive(InstalledPackage package)
{ {
RemoveInstalledPackageAndUpdateActive(package.Id); RemoveInstalledPackageAndUpdateActive(package.Id);

10
StabilityMatrix.Core/Models/Settings/Size.cs

@ -0,0 +1,10 @@
namespace StabilityMatrix.Core.Models.Settings;
public record struct Size(double Width, double Height)
{
public static Size operator +(Size current, Size other) =>
new(current.Width + other.Width, current.Height + other.Height);
public static Size operator -(Size current, Size other) =>
new(current.Width - other.Width, current.Height - other.Height);
}

3
StabilityMatrix.Core/Services/ISettingsManager.cs

@ -69,7 +69,8 @@ public interface ISettingsManager
T source, T source,
Expression<Func<T, TValue>> sourceProperty, Expression<Func<T, TValue>> sourceProperty,
Expression<Func<Settings, TValue>> settingsProperty, Expression<Func<Settings, TValue>> settingsProperty,
bool setInitial = false bool setInitial = false,
TimeSpan? delay = null
) )
where T : INotifyPropertyChanged; where T : INotifyPropertyChanged;

43
StabilityMatrix.Core/Services/SettingsManager.cs

@ -165,7 +165,8 @@ public class SettingsManager : ISettingsManager
T source, T source,
Expression<Func<T, TValue>> sourceProperty, Expression<Func<T, TValue>> sourceProperty,
Expression<Func<Settings, TValue>> settingsProperty, Expression<Func<Settings, TValue>> settingsProperty,
bool setInitial = false bool setInitial = false,
TimeSpan? delay = null
) )
where T : INotifyPropertyChanged where T : INotifyPropertyChanged
{ {
@ -216,9 +217,16 @@ public class SettingsManager : ISettingsManager
settingsSetter(Settings, sourceGetter(source)); settingsSetter(Settings, sourceGetter(source));
if (IsLibraryDirSet) if (IsLibraryDirSet)
{
if (delay != null)
{
SaveSettingsDelayed(delay.Value).SafeFireAndForget();
}
else
{ {
SaveSettingsAsync().SafeFireAndForget(); SaveSettingsAsync().SafeFireAndForget();
} }
}
else else
{ {
Logger.Warn("[RelayPropertyFor] LibraryDir not set when saving"); Logger.Warn("[RelayPropertyFor] LibraryDir not set when saving");
@ -656,4 +664,37 @@ public class SettingsManager : ISettingsManager
{ {
return Task.Run(SaveSettings); return Task.Run(SaveSettings);
} }
private CancellationTokenSource? delayedSaveCts;
private Task SaveSettingsDelayed(TimeSpan delay)
{
var cts = new CancellationTokenSource();
var oldCancellationToken = Interlocked.Exchange(ref delayedSaveCts, cts);
try
{
oldCancellationToken?.Cancel();
}
catch (ObjectDisposedException) { }
return Task.Run(
async () =>
{
try
{
await Task.Delay(delay, cts.Token);
await SaveSettingsAsync();
}
catch (TaskCanceledException) { }
finally
{
cts.Dispose();
}
},
CancellationToken.None
);
}
} }

Loading…
Cancel
Save