Browse Source

Merge branch 'dev' into inference-modules

# Conflicts:
#	StabilityMatrix.Core/StabilityMatrix.Core.csproj
pull/333/head
Ionite 12 months ago
parent
commit
7b58e39487
No known key found for this signature in database
  1. 2
      .config/dotnet-tools.json
  2. 18
      CHANGELOG.md
  3. 2
      StabilityMatrix.Avalonia/App.axaml
  4. 34
      StabilityMatrix.Avalonia/Assets/sitecustomize.py
  5. 304
      StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs
  6. 43
      StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs
  7. 113
      StabilityMatrix.Avalonia/Controls/Extensions/ShowDisabledTooltipExtension.cs
  8. 37
      StabilityMatrix.Avalonia/Controls/LineDashFrame.cs
  9. 127
      StabilityMatrix.Avalonia/Controls/ProgressRing.cs
  10. 22
      StabilityMatrix.Avalonia/Converters/CustomStringFormatConverter.cs
  11. 31
      StabilityMatrix.Avalonia/Converters/IndexPlusOneConverter.cs
  12. 41
      StabilityMatrix.Avalonia/Converters/MemoryBytesFormatter.cs
  13. 8
      StabilityMatrix.Avalonia/Converters/StringFormatConverters.cs
  14. 2
      StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs
  15. 99
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  16. 33
      StabilityMatrix.Avalonia/Languages/Resources.resx
  17. 55
      StabilityMatrix.Avalonia/Program.cs
  18. 2
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  19. 46
      StabilityMatrix.Avalonia/Styles/ProgressRing.axaml
  20. 45
      StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs
  21. 8
      StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs
  22. 208
      StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs
  23. 244
      StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
  24. 5
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  25. 6
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs
  26. 148
      StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml
  27. 243
      StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml
  28. 3
      StabilityMatrix.Avalonia/Views/SettingsPage.axaml
  29. 46
      StabilityMatrix.Core/Database/LiteDbContext.cs
  30. 172
      StabilityMatrix.Core/Helper/HardwareHelper.cs
  31. 7
      StabilityMatrix.Core/Helper/HardwareInfo/CpuInfo.cs
  32. 31
      StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs
  33. 234
      StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs
  34. 10
      StabilityMatrix.Core/Helper/HardwareInfo/MemoryInfo.cs
  35. 9
      StabilityMatrix.Core/Helper/HardwareInfo/MemoryLevel.cs
  36. 19
      StabilityMatrix.Core/Helper/HardwareInfo/Win32MemoryStatusEx.cs
  37. 37
      StabilityMatrix.Core/Helper/Size.cs
  38. 8
      StabilityMatrix.Core/Models/GlobalConfig.cs
  39. 20
      StabilityMatrix.Core/Models/InstalledPackage.cs
  40. 7
      StabilityMatrix.Core/Models/LaunchOptionType.cs
  41. 26
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  42. 64
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  43. 99
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  44. 20
      StabilityMatrix.Core/Models/Packages/Fooocus.cs
  45. 34
      StabilityMatrix.Core/Models/Packages/KohyaSs.cs
  46. 30
      StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs
  47. 88
      StabilityMatrix.Core/Models/Packages/VladAutomatic.cs
  48. 14
      StabilityMatrix.Core/Models/Settings/Settings.cs
  49. 113
      StabilityMatrix.Core/Services/SettingsManager.cs
  50. 2
      StabilityMatrix.Core/StabilityMatrix.Core.csproj
  51. 35
      StabilityMatrix.Tests/Core/DefaultUnknownEnumConverterTests.cs

2
.config/dotnet-tools.json

@ -15,7 +15,7 @@
] ]
}, },
"csharpier": { "csharpier": {
"version": "0.25.0", "version": "0.26.3",
"commands": [ "commands": [
"dotnet-csharpier" "dotnet-csharpier"
] ]

18
CHANGELOG.md

@ -5,6 +5,24 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.7.0-pre.2
### Added
- Added System Information section to Settings
### Changed
- Moved Inference Settings to subpage
### Fixed
- Fixed crash when loading an empty settings file
- Improve Settings save and load performance with .NET 8 Source Generating Serialization
- Fixed ApplicationException during database shutdown
## v2.7.0-pre.1
### Fixed
- Fixed control character decoding that caused some progress bars to show as `\u2588`
- Fixed Python `rich` package's progress bars not showing in console
- Optimized ProgressRing animation bindings to reduce CPU usage
- Improved safety checks in custom control rendering to reduce potential graphical artifacts
- Improved console rendering safety with cursor line increment clamping, as potential fix for [#111](https://github.com/LykosAI/StabilityMatrix/issues/111)
## v2.7.0-dev.4 ## v2.7.0-dev.4
### Fixed ### Fixed
- Fixed [#290](https://github.com/LykosAI/StabilityMatrix/issues/290) - Model browser crash due to text trimming certain unicode characters - Fixed [#290](https://github.com/LykosAI/StabilityMatrix/issues/290) - Model browser crash due to text trimming certain unicode characters

2
StabilityMatrix.Avalonia/App.axaml

@ -31,8 +31,6 @@
<idcr:ControlRecycling x:Key="ControlRecyclingKey" /> <idcr:ControlRecycling x:Key="ControlRecyclingKey" />
<x:Double x:Key="ContentDialogMaxWidth">700</x:Double> <x:Double x:Key="ContentDialogMaxWidth">700</x:Double>
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">32</x:Double>
<SolidColorBrush x:Key="ToolTipBackground" Color="#1E1F22"/> <SolidColorBrush x:Key="ToolTipBackground" Color="#1E1F22"/>
<SolidColorBrush x:Key="ToolTipForeground" Color="#9FBDC3"/> <SolidColorBrush x:Key="ToolTipForeground" Color="#9FBDC3"/>
<FontFamily x:Key="NotoSansJP">avares://StabilityMatrix.Avalonia/Assets/Fonts/NotoSansJP#Noto Sans JP</FontFamily> <FontFamily x:Key="NotoSansJP">avares://StabilityMatrix.Avalonia/Assets/Fonts/NotoSansJP#Noto Sans JP</FontFamily>

34
StabilityMatrix.Avalonia/Assets/sitecustomize.py

@ -46,7 +46,41 @@ def audit(event: str, *args):
# Reconfigure stdout to UTF-8 # Reconfigure stdout to UTF-8
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
# Install the audit hook # Install the audit hook
sys.addaudithook(audit) sys.addaudithook(audit)
# Patch Rich terminal detection
def _patch_rich_console():
try:
from rich import console
class _Console(console.Console):
@property
def is_terminal(self) -> bool:
return True
console.Console = _Console
except ImportError:
pass
except Exception as e:
print("[sitecustomize error]:", e)
_patch_rich_console()
# Patch tqdm to use stdout instead of stderr
def _patch_tqdm():
try:
import sys
from tqdm import std
sys.stderr = sys.stdout
except ImportError:
pass
except Exception as e:
print("[sitecustomize error]:", e)
_patch_tqdm()

304
StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs

@ -54,11 +54,7 @@ public class AdvancedImageBox : TemplatedControl
remove => _propertyChanged -= value; remove => _propertyChanged -= value;
} }
protected bool RaiseAndSetIfChanged<T>( protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
ref T field,
T value,
[CallerMemberName] string? propertyName = null
)
{ {
if (EqualityComparer<T>.Default.Equals(field, value)) if (EqualityComparer<T>.Default.Equals(field, value))
return false; return false;
@ -335,9 +331,7 @@ public class AdvancedImageBox : TemplatedControl
if (index < Count - 1) if (index < Count - 1)
index++; index++;
return constrainZoomLevel > 0 && this[index] >= constrainZoomLevel return constrainZoomLevel > 0 && this[index] >= constrainZoomLevel ? constrainZoomLevel : this[index];
? constrainZoomLevel
: this[index];
} }
/// <summary> /// <summary>
@ -352,9 +346,7 @@ public class AdvancedImageBox : TemplatedControl
if (index > 0) if (index > 0)
index--; index--;
return constrainZoomLevel > 0 && this[index] <= constrainZoomLevel return constrainZoomLevel > 0 && this[index] <= constrainZoomLevel ? constrainZoomLevel : this[index];
? constrainZoomLevel
: this[index];
} }
/// <summary> /// <summary>
@ -525,11 +517,10 @@ public class AdvancedImageBox : TemplatedControl
#endregion #endregion
#region Properties #region Properties
public static readonly DirectProperty<AdvancedImageBox, bool> CanRenderProperty = public static readonly DirectProperty<AdvancedImageBox, bool> CanRenderProperty = AvaloniaProperty.RegisterDirect<
AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>( AdvancedImageBox,
nameof(CanRender), bool
o => o.CanRender >(nameof(CanRender), o => o.CanRender);
);
/// <summary> /// <summary>
/// Gets or sets if control can render the image /// Gets or sets if control can render the image
@ -560,11 +551,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(GridCellSizeProperty, value); set => SetValue(GridCellSizeProperty, value);
} }
public static readonly StyledProperty<ISolidColorBrush> GridColorProperty = public static readonly StyledProperty<ISolidColorBrush> GridColorProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>( AdvancedImageBox,
nameof(GridColor), ISolidColorBrush
SolidColorBrush.Parse("#181818") >(nameof(GridColor), SolidColorBrush.Parse("#181818"));
);
/// <summary> /// <summary>
/// Gets or sets the color used to create the checkerboard style background /// Gets or sets the color used to create the checkerboard style background
@ -575,11 +565,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(GridColorProperty, value); set => SetValue(GridColorProperty, value);
} }
public static readonly StyledProperty<ISolidColorBrush> GridColorAlternateProperty = public static readonly StyledProperty<ISolidColorBrush> GridColorAlternateProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>( AdvancedImageBox,
nameof(GridColorAlternate), ISolidColorBrush
SolidColorBrush.Parse("#252525") >(nameof(GridColorAlternate), SolidColorBrush.Parse("#252525"));
);
/// <summary> /// <summary>
/// Gets or sets the color used to create the checkerboard style background /// Gets or sets the color used to create the checkerboard style background
@ -606,7 +595,8 @@ public class AdvancedImageBox : TemplatedControl
{ {
var loader = ImageLoader.AsyncImageLoader; var loader = ImageLoader.AsyncImageLoader;
Dispatcher.UIThread Dispatcher
.UIThread
.InvokeAsync(async () => .InvokeAsync(async () =>
{ {
Image = await loader.ProvideImageAsync(value); Image = await loader.ProvideImageAsync(value);
@ -616,10 +606,9 @@ public class AdvancedImageBox : TemplatedControl
} }
} }
public static readonly StyledProperty<Bitmap?> ImageProperty = AvaloniaProperty.Register< public static readonly StyledProperty<Bitmap?> ImageProperty = AvaloniaProperty.Register<AdvancedImageBox, Bitmap?>(
AdvancedImageBox, nameof(Image)
Bitmap? );
>(nameof(Image));
/// <summary> /// <summary>
/// Gets or sets the image to be displayed /// Gets or sets the image to be displayed
@ -656,8 +645,7 @@ public class AdvancedImageBox : TemplatedControl
{ {
offsetBackup = Offset; offsetBackup = Offset;
var zoomFactorScale = var zoomFactorScale = (float)GetZoomLevelToFit(newImage) / GetZoomLevelToFit(oldImage);
(float)GetZoomLevelToFit(newImage) / GetZoomLevelToFit(oldImage);
var imageScale = newImage.Size / oldImage.Size; var imageScale = newImage.Size / oldImage.Size;
Debug.WriteLine($"Image scale: {imageScale}"); Debug.WriteLine($"Image scale: {imageScale}");
@ -731,8 +719,10 @@ public class AdvancedImageBox : TemplatedControl
public bool HaveTrackerImage => _trackerImage is not null; public bool HaveTrackerImage => _trackerImage is not null;
public static readonly StyledProperty<bool> TrackerImageAutoZoomProperty = public static readonly StyledProperty<bool> TrackerImageAutoZoomProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(TrackerImageAutoZoom), true); AdvancedImageBox,
bool
>(nameof(TrackerImageAutoZoom), true);
/// <summary> /// <summary>
/// Gets or sets if the tracker image will be scaled to the current zoom /// Gets or sets if the tracker image will be scaled to the current zoom
@ -743,8 +733,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(TrackerImageAutoZoomProperty, value); set => SetValue(TrackerImageAutoZoomProperty, value);
} }
public static readonly StyledProperty<bool> IsTrackerImageEnabledProperty = public static readonly StyledProperty<bool> IsTrackerImageEnabledProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<AdvancedImageBox, bool>("IsTrackerImageEnabled"); AdvancedImageBox,
bool
>("IsTrackerImageEnabled");
public bool IsTrackerImageEnabled public bool IsTrackerImageEnabled
{ {
@ -776,10 +768,10 @@ public class AdvancedImageBox : TemplatedControl
} }
} }
public static readonly StyledProperty<bool> ShowGridProperty = AvaloniaProperty.Register< public static readonly StyledProperty<bool> ShowGridProperty = AvaloniaProperty.Register<AdvancedImageBox, bool>(
AdvancedImageBox, nameof(ShowGrid),
bool true
>(nameof(ShowGrid), true); );
/// <summary> /// <summary>
/// Gets or sets the grid visibility when reach high zoom levels /// Gets or sets the grid visibility when reach high zoom levels
@ -791,10 +783,7 @@ public class AdvancedImageBox : TemplatedControl
} }
public static readonly DirectProperty<AdvancedImageBox, Point> PointerPositionProperty = public static readonly DirectProperty<AdvancedImageBox, Point> PointerPositionProperty =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, Point>( AvaloniaProperty.RegisterDirect<AdvancedImageBox, Point>(nameof(PointerPosition), o => o.PointerPosition);
nameof(PointerPosition),
o => o.PointerPosition
);
/// <summary> /// <summary>
/// Gets the current pointer position /// Gets the current pointer position
@ -805,11 +794,10 @@ public class AdvancedImageBox : TemplatedControl
private set => SetAndRaise(PointerPositionProperty, ref _pointerPosition, value); private set => SetAndRaise(PointerPositionProperty, ref _pointerPosition, value);
} }
public static readonly DirectProperty<AdvancedImageBox, bool> IsPanningProperty = public static readonly DirectProperty<AdvancedImageBox, bool> IsPanningProperty = AvaloniaProperty.RegisterDirect<
AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>( AdvancedImageBox,
nameof(IsPanning), bool
o => o.IsPanning >(nameof(IsPanning), o => o.IsPanning);
);
/// <summary> /// <summary>
/// Gets if control is currently panning /// Gets if control is currently panning
@ -836,11 +824,10 @@ public class AdvancedImageBox : TemplatedControl
} }
} }
public static readonly DirectProperty<AdvancedImageBox, bool> IsSelectingProperty = public static readonly DirectProperty<AdvancedImageBox, bool> IsSelectingProperty = AvaloniaProperty.RegisterDirect<
AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>( AdvancedImageBox,
nameof(IsSelecting), bool
o => o.IsSelecting >(nameof(IsSelecting), o => o.IsSelecting);
);
/// <summary> /// <summary>
/// Gets if control is currently selecting a ROI /// Gets if control is currently selecting a ROI
@ -863,10 +850,10 @@ public class AdvancedImageBox : TemplatedControl
} }
} }
public static readonly StyledProperty<bool> AutoPanProperty = AvaloniaProperty.Register< public static readonly StyledProperty<bool> AutoPanProperty = AvaloniaProperty.Register<AdvancedImageBox, bool>(
AdvancedImageBox, nameof(AutoPan),
bool true
>(nameof(AutoPan), true); );
/// <summary> /// <summary>
/// Gets or sets if the control can pan with the mouse /// Gets or sets if the control can pan with the mouse
@ -877,11 +864,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(AutoPanProperty, value); set => SetValue(AutoPanProperty, value);
} }
public static readonly StyledProperty<MouseButtons> PanWithMouseButtonsProperty = public static readonly StyledProperty<MouseButtons> PanWithMouseButtonsProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<AdvancedImageBox, MouseButtons>( AdvancedImageBox,
nameof(PanWithMouseButtons), MouseButtons
MouseButtons.LeftButton | MouseButtons.MiddleButton >(nameof(PanWithMouseButtons), MouseButtons.LeftButton | MouseButtons.MiddleButton);
);
/// <summary> /// <summary>
/// Gets or sets the mouse buttons to pan the image /// Gets or sets the mouse buttons to pan the image
@ -906,11 +892,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(PanWithArrowsProperty, value); set => SetValue(PanWithArrowsProperty, value);
} }
public static readonly StyledProperty<MouseButtons> SelectWithMouseButtonsProperty = public static readonly StyledProperty<MouseButtons> SelectWithMouseButtonsProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<AdvancedImageBox, MouseButtons>( AdvancedImageBox,
nameof(SelectWithMouseButtons), MouseButtons
MouseButtons.LeftButton >(nameof(SelectWithMouseButtons), MouseButtons.LeftButton);
);
/// <summary> /// <summary>
/// Gets or sets the mouse buttons to select a region on image /// Gets or sets the mouse buttons to select a region on image
@ -935,10 +920,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(InvertMousePanProperty, value); set => SetValue(InvertMousePanProperty, value);
} }
public static readonly StyledProperty<bool> AutoCenterProperty = AvaloniaProperty.Register< public static readonly StyledProperty<bool> AutoCenterProperty = AvaloniaProperty.Register<AdvancedImageBox, bool>(
AdvancedImageBox, nameof(AutoCenter),
bool true
>(nameof(AutoCenter), true); );
/// <summary> /// <summary>
/// Gets or sets if image is auto centered /// Gets or sets if image is auto centered
@ -993,10 +978,10 @@ public class AdvancedImageBox : TemplatedControl
} }
} }
public static readonly StyledProperty<bool> AllowZoomProperty = AvaloniaProperty.Register< public static readonly StyledProperty<bool> AllowZoomProperty = AvaloniaProperty.Register<AdvancedImageBox, bool>(
AdvancedImageBox, nameof(AllowZoom),
bool true
>(nameof(AllowZoom), true); );
/// <summary> /// <summary>
/// Gets or sets if zoom is allowed /// Gets or sets if zoom is allowed
@ -1007,10 +992,8 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(AllowZoomProperty, value); set => SetValue(AllowZoomProperty, value);
} }
public static readonly DirectProperty< public static readonly DirectProperty<AdvancedImageBox, ZoomLevelCollection> ZoomLevelsProperty =
AdvancedImageBox, AvaloniaProperty.RegisterDirect<AdvancedImageBox, ZoomLevelCollection>(
ZoomLevelCollection
> ZoomLevelsProperty = AvaloniaProperty.RegisterDirect<AdvancedImageBox, ZoomLevelCollection>(
nameof(ZoomLevels), nameof(ZoomLevels),
o => o.ZoomLevels, o => o.ZoomLevels,
(o, v) => o.ZoomLevels = v (o, v) => o.ZoomLevels = v
@ -1028,10 +1011,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetAndRaise(ZoomLevelsProperty, ref _zoomLevels, value); set => SetAndRaise(ZoomLevelsProperty, ref _zoomLevels, value);
} }
public static readonly StyledProperty<int> MinZoomProperty = AvaloniaProperty.Register< public static readonly StyledProperty<int> MinZoomProperty = AvaloniaProperty.Register<AdvancedImageBox, int>(
AdvancedImageBox, nameof(MinZoom),
int 10
>(nameof(MinZoom), 10); );
/// <summary> /// <summary>
/// Gets or sets the minimum possible zoom. /// Gets or sets the minimum possible zoom.
@ -1043,10 +1026,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(MinZoomProperty, value); set => SetValue(MinZoomProperty, value);
} }
public static readonly StyledProperty<int> MaxZoomProperty = AvaloniaProperty.Register< public static readonly StyledProperty<int> MaxZoomProperty = AvaloniaProperty.Register<AdvancedImageBox, int>(
AdvancedImageBox, nameof(MaxZoom),
int 6400
>(nameof(MaxZoom), 6400); );
/// <summary> /// <summary>
/// Gets or sets the maximum possible zoom. /// Gets or sets the maximum possible zoom.
@ -1058,8 +1041,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(MaxZoomProperty, value); set => SetValue(MaxZoomProperty, value);
} }
public static readonly StyledProperty<bool> ConstrainZoomOutToFitLevelProperty = public static readonly StyledProperty<bool> ConstrainZoomOutToFitLevelProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(ConstrainZoomOutToFitLevel), true); AdvancedImageBox,
bool
>(nameof(ConstrainZoomOutToFitLevel), true);
/// <summary> /// <summary>
/// Gets or sets if the zoom out should constrain to fit image as the lowest zoom level. /// Gets or sets if the zoom out should constrain to fit image as the lowest zoom level.
@ -1070,8 +1055,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(ConstrainZoomOutToFitLevelProperty, value); set => SetValue(ConstrainZoomOutToFitLevelProperty, value);
} }
public static readonly DirectProperty<AdvancedImageBox, int> OldZoomProperty = public static readonly DirectProperty<AdvancedImageBox, int> OldZoomProperty = AvaloniaProperty.RegisterDirect<
AvaloniaProperty.RegisterDirect<AdvancedImageBox, int>(nameof(OldZoom), o => o.OldZoom); AdvancedImageBox,
int
>(nameof(OldZoom), o => o.OldZoom);
private int _oldZoom = 100; private int _oldZoom = 100;
@ -1085,10 +1072,10 @@ public class AdvancedImageBox : TemplatedControl
private set => SetAndRaise(OldZoomProperty, ref _oldZoom, value); private set => SetAndRaise(OldZoomProperty, ref _oldZoom, value);
} }
public static readonly StyledProperty<int> ZoomProperty = AvaloniaProperty.Register< public static readonly StyledProperty<int> ZoomProperty = AvaloniaProperty.Register<AdvancedImageBox, int>(
AdvancedImageBox, nameof(Zoom),
int 100
>(nameof(Zoom), 100); );
/// <summary> /// <summary>
/// Gets or sets the zoom. /// Gets or sets the zoom.
@ -1178,11 +1165,10 @@ public class AdvancedImageBox : TemplatedControl
/// <value>The height of the scaled image.</value> /// <value>The height of the scaled image.</value>
public double ScaledImageHeight => Image?.Size.Height * ZoomFactor ?? 0; public double ScaledImageHeight => Image?.Size.Height * ZoomFactor ?? 0;
public static readonly StyledProperty<ISolidColorBrush> PixelGridColorProperty = public static readonly StyledProperty<ISolidColorBrush> PixelGridColorProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>( AdvancedImageBox,
nameof(PixelGridColor), ISolidColorBrush
Brushes.DimGray >(nameof(PixelGridColor), Brushes.DimGray);
);
/// <summary> /// <summary>
/// Gets or sets the color of the pixel grid. /// Gets or sets the color of the pixel grid.
@ -1194,8 +1180,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(PixelGridColorProperty, value); set => SetValue(PixelGridColorProperty, value);
} }
public static readonly StyledProperty<int> PixelGridZoomThresholdProperty = public static readonly StyledProperty<int> PixelGridZoomThresholdProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<AdvancedImageBox, int>(nameof(PixelGridZoomThreshold), 13); AdvancedImageBox,
int
>(nameof(PixelGridZoomThreshold), 13);
/// <summary> /// <summary>
/// Minimum size of zoomed pixel's before the pixel grid will be drawn /// Minimum size of zoomed pixel's before the pixel grid will be drawn
@ -1206,11 +1194,15 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(PixelGridZoomThresholdProperty, value); set => SetValue(PixelGridZoomThresholdProperty, value);
} }
public static readonly StyledProperty<SelectionModes> SelectionModeProperty = public static readonly StyledProperty<SelectionModes> SelectionModeProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<AdvancedImageBox, SelectionModes>(nameof(SelectionMode)); AdvancedImageBox,
SelectionModes
>(nameof(SelectionMode));
public static readonly StyledProperty<bool> IsPixelGridEnabledProperty = public static readonly StyledProperty<bool> IsPixelGridEnabledProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<AdvancedImageBox, bool>("IsPixelGridEnabled", true); AdvancedImageBox,
bool
>("IsPixelGridEnabled", true);
/// <summary> /// <summary>
/// Whether or not to draw the pixel grid at the <see cref="PixelGridZoomThreshold"/> /// Whether or not to draw the pixel grid at the <see cref="PixelGridZoomThreshold"/>
@ -1227,11 +1219,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(SelectionModeProperty, value); set => SetValue(SelectionModeProperty, value);
} }
public static readonly StyledProperty<ISolidColorBrush> SelectionColorProperty = public static readonly StyledProperty<ISolidColorBrush> SelectionColorProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>( AdvancedImageBox,
nameof(SelectionColor), ISolidColorBrush
new SolidColorBrush(new Color(127, 0, 128, 255)) >(nameof(SelectionColor), new SolidColorBrush(new Color(127, 0, 128, 255)));
);
public ISolidColorBrush SelectionColor public ISolidColorBrush SelectionColor
{ {
@ -1313,14 +1304,9 @@ public class AdvancedImageBox : TemplatedControl
AffectsRender<AdvancedImageBox>(ShowGridProperty); AffectsRender<AdvancedImageBox>(ShowGridProperty);
HorizontalScrollBar = HorizontalScrollBar =
e.NameScope.Find<ScrollBar>("PART_HorizontalScrollBar") e.NameScope.Find<ScrollBar>("PART_HorizontalScrollBar") ?? throw new NullReferenceException();
?? throw new NullReferenceException(); VerticalScrollBar = e.NameScope.Find<ScrollBar>("PART_VerticalScrollBar") ?? throw new NullReferenceException();
VerticalScrollBar = ViewPort = e.NameScope.Find<ContentPresenter>("PART_ViewPort") ?? throw new NullReferenceException();
e.NameScope.Find<ScrollBar>("PART_VerticalScrollBar")
?? throw new NullReferenceException();
ViewPort =
e.NameScope.Find<ContentPresenter>("PART_ViewPort")
?? throw new NullReferenceException();
SizeModeChanged(); SizeModeChanged();
@ -1347,13 +1333,12 @@ public class AdvancedImageBox : TemplatedControl
// If we're in high zoom, switch off bitmap interpolation mode // If we're in high zoom, switch off bitmap interpolation mode
// Otherwise use high quality // Otherwise use high quality
BitmapInterpolationMode = isHighZoom BitmapInterpolationMode = isHighZoom ? BitmapInterpolationMode.None : BitmapInterpolationMode.HighQuality;
? BitmapInterpolationMode.None
: BitmapInterpolationMode.HighQuality;
InvalidateVisual(); InvalidateVisual();
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RenderBackgroundGrid(DrawingContext context) private void RenderBackgroundGrid(DrawingContext context)
{ {
var size = GridCellSize; var size = GridCellSize;
@ -1387,8 +1372,6 @@ public class AdvancedImageBox : TemplatedControl
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
base.Render(context);
var gridCellSize = GridCellSize; var gridCellSize = GridCellSize;
if (ShowGrid & gridCellSize > 0 && (!IsHorizontalBarVisible || !IsVerticalBarVisible)) if (ShowGrid & gridCellSize > 0 && (!IsHorizontalBarVisible || !IsVerticalBarVisible))
@ -1399,9 +1382,7 @@ public class AdvancedImageBox : TemplatedControl
var zoomFactor = ZoomFactor; var zoomFactor = ZoomFactor;
var shouldDrawPixelGrid = var shouldDrawPixelGrid =
IsPixelGridEnabled IsPixelGridEnabled && SizeMode == SizeModes.Normal && zoomFactor > PixelGridZoomThreshold;
&& SizeMode == SizeModes.Normal
&& zoomFactor > PixelGridZoomThreshold;
// Draw Grid // Draw Grid
/*var viewPortSize = ViewPortSize; /*var viewPortSize = ViewPortSize;
@ -1441,16 +1422,10 @@ public class AdvancedImageBox : TemplatedControl
if (HaveTrackerImage && _pointerPosition is { X: >= 0, Y: >= 0 }) if (HaveTrackerImage && _pointerPosition is { X: >= 0, Y: >= 0 })
{ {
var destSize = TrackerImageAutoZoom var destSize = TrackerImageAutoZoom
? new Size( ? new Size(_trackerImage!.Size.Width * zoomFactor, _trackerImage.Size.Height * zoomFactor)
_trackerImage!.Size.Width * zoomFactor,
_trackerImage.Size.Height * zoomFactor
)
: image.Size; : image.Size;
var destPos = new Point( var destPos = new Point(_pointerPosition.X - destSize.Width / 2, _pointerPosition.Y - destSize.Height / 2);
_pointerPosition.X - destSize.Width / 2,
_pointerPosition.Y - destSize.Height / 2
);
context.DrawImage(_trackerImage!, new Rect(destPos, destSize)); context.DrawImage(_trackerImage!, new Rect(destPos, destSize));
} }
@ -1462,30 +1437,14 @@ public class AdvancedImageBox : TemplatedControl
var offsetY = Offset.Y % zoomFactor; var offsetY = Offset.Y % zoomFactor;
Pen pen = new(PixelGridColor); Pen pen = new(PixelGridColor);
for ( for (var x = imageViewPort.X + zoomFactor - offsetX; x < imageViewPort.Right; x += zoomFactor)
var x = imageViewPort.X + zoomFactor - offsetX;
x < imageViewPort.Right;
x += zoomFactor
)
{ {
context.DrawLine( context.DrawLine(pen, new Point(x, imageViewPort.X), new Point(x, imageViewPort.Bottom));
pen,
new Point(x, imageViewPort.X),
new Point(x, imageViewPort.Bottom)
);
} }
for ( for (var y = imageViewPort.Y + zoomFactor - offsetY; y < imageViewPort.Bottom; y += zoomFactor)
var y = imageViewPort.Y + zoomFactor - offsetY;
y < imageViewPort.Bottom;
y += zoomFactor
)
{ {
context.DrawLine( context.DrawLine(pen, new Point(imageViewPort.Y, y), new Point(imageViewPort.Right, y));
pen,
new Point(imageViewPort.Y, y),
new Point(imageViewPort.Right, y)
);
} }
context.DrawRectangle(pen, imageViewPort); context.DrawRectangle(pen, imageViewPort);
@ -1496,12 +1455,7 @@ public class AdvancedImageBox : TemplatedControl
var rect = GetOffsetRectangle(SelectionRegion); var rect = GetOffsetRectangle(SelectionRegion);
var selectionColor = SelectionColor; var selectionColor = SelectionColor;
context.FillRectangle(selectionColor, rect); context.FillRectangle(selectionColor, rect);
var color = Color.FromArgb( var color = Color.FromArgb(255, selectionColor.Color.R, selectionColor.Color.G, selectionColor.Color.B);
255,
selectionColor.Color.R,
selectionColor.Color.G,
selectionColor.Color.B
);
context.DrawRectangle(new Pen(color.ToUInt32()), rect); context.DrawRectangle(new Pen(color.ToUInt32()), rect);
} }
} }
@ -1609,8 +1563,7 @@ public class AdvancedImageBox : TemplatedControl
{ {
if ( if (
!( !(
pointer.Properties.IsLeftButtonPressed pointer.Properties.IsLeftButtonPressed && (SelectWithMouseButtons & MouseButtons.LeftButton) != 0
&& (SelectWithMouseButtons & MouseButtons.LeftButton) != 0
|| pointer.Properties.IsMiddleButtonPressed || pointer.Properties.IsMiddleButtonPressed
&& (SelectWithMouseButtons & MouseButtons.MiddleButton) != 0 && (SelectWithMouseButtons & MouseButtons.MiddleButton) != 0
|| pointer.Properties.IsRightButtonPressed || pointer.Properties.IsRightButtonPressed
@ -1624,12 +1577,10 @@ public class AdvancedImageBox : TemplatedControl
{ {
if ( if (
!( !(
pointer.Properties.IsLeftButtonPressed pointer.Properties.IsLeftButtonPressed && (PanWithMouseButtons & MouseButtons.LeftButton) != 0
&& (PanWithMouseButtons & MouseButtons.LeftButton) != 0
|| pointer.Properties.IsMiddleButtonPressed || pointer.Properties.IsMiddleButtonPressed
&& (PanWithMouseButtons & MouseButtons.MiddleButton) != 0 && (PanWithMouseButtons & MouseButtons.MiddleButton) != 0
|| pointer.Properties.IsRightButtonPressed || pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & MouseButtons.RightButton) != 0
&& (PanWithMouseButtons & MouseButtons.RightButton) != 0
) )
|| !AutoPan || !AutoPan
|| SizeMode != SizeModes.Normal || SizeMode != SizeModes.Normal
@ -2669,12 +2620,8 @@ public class AdvancedImageBox : TemplatedControl
case SizeModes.Normal: case SizeModes.Normal:
if (AutoCenter) if (AutoCenter)
{ {
xOffset = ( xOffset = (!IsHorizontalBarVisible ? (viewPortSize.Width - ScaledImageWidth) / 2 : 0);
!IsHorizontalBarVisible ? (viewPortSize.Width - ScaledImageWidth) / 2 : 0 yOffset = (!IsVerticalBarVisible ? (viewPortSize.Height - ScaledImageHeight) / 2 : 0);
);
yOffset = (
!IsVerticalBarVisible ? (viewPortSize.Height - ScaledImageHeight) / 2 : 0
);
} }
width = Math.Min(ScaledImageWidth - Math.Abs(Offset.X), viewPortSize.Width); width = Math.Min(ScaledImageWidth - Math.Abs(Offset.X), viewPortSize.Width);
@ -2729,12 +2676,7 @@ public class AdvancedImageBox : TemplatedControl
var pixelSize = SelectionPixelSize; var pixelSize = SelectionPixelSize;
using var frameBuffer = image.Lock(); using var frameBuffer = image.Lock();
var newBitmap = new WriteableBitmap( var newBitmap = new WriteableBitmap(pixelSize, image.Dpi, frameBuffer.Format, AlphaFormat.Unpremul);
pixelSize,
image.Dpi,
frameBuffer.Format,
AlphaFormat.Unpremul
);
using var newFrameBuffer = newBitmap.Lock(); using var newFrameBuffer = newBitmap.Lock();
var i = 0; var i = 0;

43
StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs

@ -15,11 +15,15 @@ public class BetterAdvancedImage : AdvancedImage
#region Reflection Shenanigans to access private parent fields #region Reflection Shenanigans to access private parent fields
[NotNull] [NotNull]
private static readonly FieldInfo? IsCornerRadiusUsedField = typeof(AdvancedImage).GetField( private static readonly FieldInfo? IsCornerRadiusUsedField = typeof(AdvancedImage).GetField(
"_isCornerRadiusUsed",BindingFlags.Instance | BindingFlags.NonPublic); "_isCornerRadiusUsed",
BindingFlags.Instance | BindingFlags.NonPublic
);
[NotNull] [NotNull]
private static readonly FieldInfo? CornerRadiusClipField = typeof(AdvancedImage).GetField( private static readonly FieldInfo? CornerRadiusClipField = typeof(AdvancedImage).GetField(
"_cornerRadiusClip",BindingFlags.Instance | BindingFlags.NonPublic); "_cornerRadiusClip",
BindingFlags.Instance | BindingFlags.NonPublic
);
private bool IsCornerRadiusUsed private bool IsCornerRadiusUsed
{ {
@ -48,13 +52,11 @@ public class BetterAdvancedImage : AdvancedImage
protected override Type StyleKeyOverride { get; } = typeof(AdvancedImage); protected override Type StyleKeyOverride { get; } = typeof(AdvancedImage);
public BetterAdvancedImage(Uri? baseUri) : base(baseUri) public BetterAdvancedImage(Uri? baseUri)
{ : base(baseUri) { }
}
public BetterAdvancedImage(IServiceProvider serviceProvider) : base(serviceProvider) public BetterAdvancedImage(IServiceProvider serviceProvider)
{ : base(serviceProvider) { }
}
/// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception>
/// <inheritdoc /> /// <inheritdoc />
@ -77,7 +79,8 @@ public class BetterAdvancedImage : AdvancedImage
HorizontalAlignment.Center => (int)(viewPort.Width - scaledSize.Width) / 2, HorizontalAlignment.Center => (int)(viewPort.Width - scaledSize.Width) / 2,
HorizontalAlignment.Right => (int)(viewPort.Width - scaledSize.Width), HorizontalAlignment.Right => (int)(viewPort.Width - scaledSize.Width),
// Stretch is default, use center // Stretch is default, use center
HorizontalAlignment.Stretch => (int) (viewPort.Width - scaledSize.Width) / 2, HorizontalAlignment.Stretch
=> (int)(viewPort.Width - scaledSize.Width) / 2,
_ => throw new ArgumentException(nameof(HorizontalContentAlignment)) _ => throw new ArgumentException(nameof(HorizontalContentAlignment))
}; };
var destY = VerticalContentAlignment switch var destY = VerticalContentAlignment switch
@ -89,11 +92,7 @@ public class BetterAdvancedImage : AdvancedImage
_ => throw new ArgumentException(nameof(VerticalContentAlignment)) _ => throw new ArgumentException(nameof(VerticalContentAlignment))
}; };
var destRect = viewPort var destRect = viewPort.CenterRect(new Rect(scaledSize)).WithX(destX).WithY(destY).Intersect(viewPort);
.CenterRect(new Rect(scaledSize))
.WithX(destX)
.WithY(destY)
.Intersect(viewPort);
var destRectUnscaledSize = destRect.Size / scale; var destRectUnscaledSize = destRect.Size / scale;
// Calculate starting points for source // Calculate starting points for source
@ -103,7 +102,8 @@ public class BetterAdvancedImage : AdvancedImage
HorizontalAlignment.Center => (int)(sourceSize - destRectUnscaledSize).Width / 2, HorizontalAlignment.Center => (int)(sourceSize - destRectUnscaledSize).Width / 2,
HorizontalAlignment.Right => (int)(sourceSize - destRectUnscaledSize).Width, HorizontalAlignment.Right => (int)(sourceSize - destRectUnscaledSize).Width,
// Stretch is default, use center // Stretch is default, use center
HorizontalAlignment.Stretch => (int) (sourceSize - destRectUnscaledSize).Width / 2, HorizontalAlignment.Stretch
=> (int)(sourceSize - destRectUnscaledSize).Width / 2,
_ => throw new ArgumentException(nameof(HorizontalContentAlignment)) _ => throw new ArgumentException(nameof(HorizontalContentAlignment))
}; };
var sourceY = VerticalContentAlignment switch var sourceY = VerticalContentAlignment switch
@ -120,10 +120,17 @@ public class BetterAdvancedImage : AdvancedImage
.WithX(sourceX) .WithX(sourceX)
.WithY(sourceY); .WithY(sourceY);
DrawingContext.PushedState? pushedState = if (IsCornerRadiusUsed)
IsCornerRadiusUsed ? context.PushClip(CornerRadiusClip) : null; {
using (context.PushClip(CornerRadiusClip))
{
context.DrawImage(source, sourceRect, destRect); context.DrawImage(source, sourceRect, destRect);
pushedState?.Dispose(); }
}
else
{
context.DrawImage(source, sourceRect, destRect);
}
} }
else else
{ {

113
StabilityMatrix.Avalonia/Controls/Extensions/ShowDisabledTooltipExtension.cs

@ -0,0 +1,113 @@
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using JetBrains.Annotations;
namespace StabilityMatrix.Avalonia.Controls.Extensions;
/// <summary>
/// Show tooltip on Controls with IsEffectivelyEnabled = false
/// https://github.com/AvaloniaUI/Avalonia/issues/3847#issuecomment-1618790059
/// </summary>
[PublicAPI]
public static class ShowDisabledTooltipExtension
{
static ShowDisabledTooltipExtension()
{
ShowOnDisabledProperty.Changed.AddClassHandler<Control>(HandleShowOnDisabledChanged);
}
public static bool GetShowOnDisabled(AvaloniaObject obj)
{
return obj.GetValue(ShowOnDisabledProperty);
}
public static void SetShowOnDisabled(AvaloniaObject obj, bool value)
{
obj.SetValue(ShowOnDisabledProperty, value);
}
public static readonly AttachedProperty<bool> ShowOnDisabledProperty = AvaloniaProperty.RegisterAttached<
object,
Control,
bool
>("ShowOnDisabled");
private static void HandleShowOnDisabledChanged(Control control, AvaloniaPropertyChangedEventArgs e)
{
if (e.GetNewValue<bool>())
{
control.DetachedFromVisualTree += AttachedControl_DetachedFromVisualOrExtension;
control.AttachedToVisualTree += AttachedControl_AttachedToVisualTree;
if (control.IsInitialized)
{
// enabled after visual attached
AttachedControl_AttachedToVisualTree(control, null!);
}
}
else
{
AttachedControl_DetachedFromVisualOrExtension(control, null!);
}
}
private static void AttachedControl_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
if (sender is not Control control || TopLevel.GetTopLevel(control) is not { } tl)
{
return;
}
// NOTE pointermove needed to be tunneled for me but you may not need to...
tl.AddHandler(InputElement.PointerMovedEvent, TopLevel_PointerMoved, RoutingStrategies.Tunnel);
}
private static void AttachedControl_DetachedFromVisualOrExtension(object? s, VisualTreeAttachmentEventArgs e)
{
if (s is not Control control)
{
return;
}
control.DetachedFromVisualTree -= AttachedControl_DetachedFromVisualOrExtension;
control.AttachedToVisualTree -= AttachedControl_AttachedToVisualTree;
if (TopLevel.GetTopLevel(control) is not { } tl)
{
return;
}
tl.RemoveHandler(InputElement.PointerMovedEvent, TopLevel_PointerMoved);
}
private static void TopLevel_PointerMoved(object? sender, PointerEventArgs e)
{
if (sender is not Control tl)
{
return;
}
var attachedControls = tl.GetVisualDescendants().Where(GetShowOnDisabled).Cast<Control>().ToList();
// find disabled children under pointer w/ this extension enabled
var disabledChildUnderPointer = attachedControls.FirstOrDefault(
x =>
x.Bounds.Contains(e.GetPosition(x.Parent as Visual))
&& x is { IsEffectivelyVisible: true, IsEffectivelyEnabled: false }
);
if (disabledChildUnderPointer != null)
{
// manually show tooltip
ToolTip.SetIsOpen(disabledChildUnderPointer, true);
}
var disabledTooltipsToHide = attachedControls.Where(
x => ToolTip.GetIsOpen(x) && x != disabledChildUnderPointer && !x.IsEffectivelyEnabled
);
foreach (var control in disabledTooltipsToHide)
{
ToolTip.SetIsOpen(control, false);
}
}
}

37
StabilityMatrix.Avalonia/Controls/LineDashFrame.cs

@ -12,8 +12,10 @@ public class LineDashFrame : Frame
{ {
protected override Type StyleKeyOverride { get; } = typeof(Frame); protected override Type StyleKeyOverride { get; } = typeof(Frame);
public static readonly StyledProperty<ISolidColorBrush> StrokeProperty = public static readonly StyledProperty<ISolidColorBrush> StrokeProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<LineDashFrame, ISolidColorBrush>("Stroke"); LineDashFrame,
ISolidColorBrush
>("Stroke");
public ISolidColorBrush Stroke public ISolidColorBrush Stroke
{ {
@ -21,8 +23,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeProperty, value); set => SetValue(StrokeProperty, value);
} }
public static readonly StyledProperty<double> StrokeThicknessProperty = public static readonly StyledProperty<double> StrokeThicknessProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<LineDashFrame, double>("StrokeThickness"); LineDashFrame,
double
>("StrokeThickness");
public double StrokeThickness public double StrokeThickness
{ {
@ -30,8 +34,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeThicknessProperty, value); set => SetValue(StrokeThicknessProperty, value);
} }
public static readonly StyledProperty<double> StrokeDashLineProperty = public static readonly StyledProperty<double> StrokeDashLineProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<LineDashFrame, double>("StrokeDashLine"); LineDashFrame,
double
>("StrokeDashLine");
public double StrokeDashLine public double StrokeDashLine
{ {
@ -39,8 +45,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeDashLineProperty, value); set => SetValue(StrokeDashLineProperty, value);
} }
public static readonly StyledProperty<double> StrokeDashSpaceProperty = public static readonly StyledProperty<double> StrokeDashSpaceProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<LineDashFrame, double>("StrokeDashSpace"); LineDashFrame,
double
>("StrokeDashSpace");
public double StrokeDashSpace public double StrokeDashSpace
{ {
@ -48,8 +56,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeDashSpaceProperty, value); set => SetValue(StrokeDashSpaceProperty, value);
} }
public static readonly StyledProperty<ISolidColorBrush> FillProperty = public static readonly StyledProperty<ISolidColorBrush> FillProperty = AvaloniaProperty.Register<
AvaloniaProperty.Register<LineDashFrame, ISolidColorBrush>("Fill"); LineDashFrame,
ISolidColorBrush
>("Fill");
public ISolidColorBrush Fill public ISolidColorBrush Fill
{ {
@ -82,17 +92,12 @@ public class LineDashFrame : Frame
/// <inheritdoc /> /// <inheritdoc />
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
base.Render(context);
var width = Bounds.Width; var width = Bounds.Width;
var height = Bounds.Height; var height = Bounds.Height;
context.DrawRectangle(Fill, null, new Rect(0, 0, width, height)); context.DrawRectangle(Fill, null, new Rect(0, 0, width, height));
var dashPen = new Pen(Stroke, StrokeThickness) var dashPen = new Pen(Stroke, StrokeThickness) { DashStyle = new DashStyle(GetDashArray(width), 0) };
{
DashStyle = new DashStyle(GetDashArray(width), 0)
};
context.DrawLine(dashPen, new Point(0, 0), new Point(width, 0)); context.DrawLine(dashPen, new Point(0, 0), new Point(width, 0));
context.DrawLine(dashPen, new Point(0, height), new Point(width, height)); context.DrawLine(dashPen, new Point(0, height), new Point(width, height));

127
StabilityMatrix.Avalonia/Controls/ProgressRing.cs

@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis; using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;
@ -13,34 +15,11 @@ namespace StabilityMatrix.Avalonia.Controls;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class ProgressRing : RangeBase public class ProgressRing : RangeBase
{ {
public static readonly StyledProperty<bool> IsIndeterminateProperty = private Arc? fillArc;
ProgressBar.IsIndeterminateProperty.AddOwner<ProgressRing>();
public static readonly StyledProperty<bool> PreserveAspectProperty = public static readonly StyledProperty<bool> IsIndeterminateProperty = ProgressBar
AvaloniaProperty.Register<ProgressRing, bool>(nameof(PreserveAspect), true); .IsIndeterminateProperty
.AddOwner<ProgressRing>();
public static readonly StyledProperty<double> ValueAngleProperty =
AvaloniaProperty.Register<ProgressRing, double>(nameof(ValueAngle));
public static readonly StyledProperty<double> StartAngleProperty =
AvaloniaProperty.Register<ProgressRing, double>(nameof(StartAngle));
public static readonly StyledProperty<double> EndAngleProperty =
AvaloniaProperty.Register<ProgressRing, double>(nameof(EndAngle), 360);
static ProgressRing()
{
MinimumProperty.Changed.AddClassHandler<ProgressRing>(OnMinimumPropertyChanged);
MaximumProperty.Changed.AddClassHandler<ProgressRing>(OnMaximumPropertyChanged);
ValueProperty.Changed.AddClassHandler<ProgressRing>(OnValuePropertyChanged);
MaximumProperty.Changed.AddClassHandler<ProgressRing>(OnStartAnglePropertyChanged);
MaximumProperty.Changed.AddClassHandler<ProgressRing>(OnEndAnglePropertyChanged);
}
public ProgressRing()
{
UpdatePseudoClasses(IsIndeterminate, PreserveAspect);
}
public bool IsIndeterminate public bool IsIndeterminate
{ {
@ -48,35 +27,90 @@ public class ProgressRing : RangeBase
set => SetValue(IsIndeterminateProperty, value); set => SetValue(IsIndeterminateProperty, value);
} }
public static readonly StyledProperty<bool> PreserveAspectProperty = AvaloniaProperty.Register<ProgressRing, bool>(
nameof(PreserveAspect),
true
);
public bool PreserveAspect public bool PreserveAspect
{ {
get => GetValue(PreserveAspectProperty); get => GetValue(PreserveAspectProperty);
set => SetValue(PreserveAspectProperty, value); set => SetValue(PreserveAspectProperty, value);
} }
public double ValueAngle public static readonly StyledProperty<double> StrokeThicknessProperty = Shape
.StrokeThicknessProperty
.AddOwner<ProgressRing>();
public double StrokeThickness
{ {
get => GetValue(ValueAngleProperty); get => GetValue(StrokeThicknessProperty);
private set => SetValue(ValueAngleProperty, value); set => SetValue(StrokeThicknessProperty, value);
} }
public static readonly StyledProperty<double> StartAngleProperty = AvaloniaProperty.Register<ProgressRing, double>(
nameof(StartAngle)
);
public double StartAngle public double StartAngle
{ {
get => GetValue(StartAngleProperty); get => GetValue(StartAngleProperty);
set => SetValue(StartAngleProperty, value); set => SetValue(StartAngleProperty, value);
} }
public static readonly StyledProperty<double> SweepAngleProperty = AvaloniaProperty.Register<ProgressRing, double>(
nameof(SweepAngle)
);
public double SweepAngle
{
get => GetValue(SweepAngleProperty);
set => SetValue(SweepAngleProperty, value);
}
public static readonly StyledProperty<double> EndAngleProperty = AvaloniaProperty.Register<ProgressRing, double>(
nameof(EndAngle),
360
);
public double EndAngle public double EndAngle
{ {
get => GetValue(EndAngleProperty); get => GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value); set => SetValue(EndAngleProperty, value);
} }
static ProgressRing()
{
AffectsRender<ProgressRing>(SweepAngleProperty, StartAngleProperty, EndAngleProperty);
ValueProperty.Changed.AddClassHandler<ProgressRing>(OnValuePropertyChanged);
SweepAngleProperty.Changed.AddClassHandler<ProgressRing>(OnSweepAnglePropertyChanged);
}
public ProgressRing()
{
UpdatePseudoClasses(IsIndeterminate, PreserveAspect);
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
fillArc = e.NameScope.Find<Arc>("PART_Fill");
if (fillArc is not null)
{
fillArc.StartAngle = StartAngle;
fillArc.SweepAngle = SweepAngle;
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
var e = change as AvaloniaPropertyChangedEventArgs<bool>; var e = change as AvaloniaPropertyChangedEventArgs<bool>;
if (e is null) return; if (e is null)
return;
if (e.Property == IsIndeterminateProperty) if (e.Property == IsIndeterminateProperty)
{ {
@ -88,9 +122,7 @@ public class ProgressRing : RangeBase
} }
} }
private void UpdatePseudoClasses( private void UpdatePseudoClasses(bool? isIndeterminate, bool? preserveAspect)
bool? isIndeterminate,
bool? preserveAspect)
{ {
if (isIndeterminate.HasValue) if (isIndeterminate.HasValue)
{ {
@ -103,28 +135,19 @@ public class ProgressRing : RangeBase
} }
} }
private static void OnMinimumPropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{
sender.Minimum = (double) e.NewValue!;
}
private static void OnMaximumPropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{
sender.Maximum = (double) e.NewValue!;
}
private static void OnValuePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) private static void OnValuePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{ {
sender.ValueAngle = ((double) e.NewValue! - sender.Minimum) * (sender.EndAngle - sender.StartAngle) / (sender.Maximum - sender.Minimum); sender.SweepAngle =
((double)e.NewValue! - sender.Minimum)
* (sender.EndAngle - sender.StartAngle)
/ (sender.Maximum - sender.Minimum);
} }
private static void OnStartAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) private static void OnSweepAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{ {
sender.StartAngle = (double) e.NewValue!; if (sender.fillArc is { } arc)
}
private static void OnEndAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
{ {
sender.EndAngle = (double) e.NewValue!; arc.SweepAngle = Math.Round(e.GetNewValue<double>());
}
} }
} }

22
StabilityMatrix.Avalonia/Converters/CustomStringFormatConverter.cs

@ -0,0 +1,22 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Data.Converters;
namespace StabilityMatrix.Avalonia.Converters;
public class CustomStringFormatConverter<T>([StringSyntax("CompositeFormat")] string format) : IValueConverter
where T : IFormatProvider, new()
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value is null ? null : string.Format(new T(), format, value);
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value is null ? null : throw new NotImplementedException();
}
}

31
StabilityMatrix.Avalonia/Converters/IndexPlusOneConverter.cs

@ -0,0 +1,31 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace StabilityMatrix.Avalonia.Converters;
/// <summary>
/// Converts an index to index + 1
/// </summary>
public class IndexPlusOneConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is int i)
{
return i + 1;
}
return value;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is int i)
{
return i - 1;
}
return value;
}
}

41
StabilityMatrix.Avalonia/Converters/MemoryBytesFormatter.cs

@ -0,0 +1,41 @@
using System;
using Size = StabilityMatrix.Core.Helper.Size;
namespace StabilityMatrix.Avalonia.Converters;
public class MemoryBytesFormatter : ICustomFormatter, IFormatProvider
{
/// <inheritdoc />
public object? GetFormat(Type? formatType)
{
return formatType == typeof(ICustomFormatter) ? this : null;
}
/// <inheritdoc />
public string Format(string? format, object? arg, IFormatProvider? formatProvider)
{
if (format == null || !format.Trim().StartsWith('M'))
{
if (arg is IFormattable formatArg)
{
return formatArg.ToString(format, formatProvider);
}
return arg?.ToString() ?? string.Empty;
}
var value = Convert.ToUInt64(arg);
var result = format.Trim().EndsWith("10", StringComparison.OrdinalIgnoreCase)
? Size.FormatBase10Bytes(value)
: Size.FormatBytes(value);
// Strip i if not Mi
if (!format.Trim().Contains('I', StringComparison.OrdinalIgnoreCase))
{
result = result.Replace("i", string.Empty, StringComparison.OrdinalIgnoreCase);
}
return result;
}
}

8
StabilityMatrix.Avalonia/Converters/StringFormatConverters.cs

@ -1,4 +1,5 @@
using Avalonia.Data.Converters; using System;
using Avalonia.Data.Converters;
namespace StabilityMatrix.Avalonia.Converters; namespace StabilityMatrix.Avalonia.Converters;
@ -7,4 +8,9 @@ public static class StringFormatConverters
private static StringFormatValueConverter? _decimalConverter; private static StringFormatValueConverter? _decimalConverter;
public static StringFormatValueConverter Decimal => public static StringFormatValueConverter Decimal =>
_decimalConverter ??= new StringFormatValueConverter("{0:D}", null); _decimalConverter ??= new StringFormatValueConverter("{0:D}", null);
private static readonly Lazy<IValueConverter> MemoryBytesConverterLazy =
new(() => new CustomStringFormatConverter<MemoryBytesFormatter>("{0:M}"));
public static IValueConverter MemoryBytes => MemoryBytesConverterLazy.Value;
} }

2
StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs

@ -84,6 +84,8 @@ public static class TextEditorConfigs
textMate.SetGrammar(scope); textMate.SetGrammar(scope);
textMate.SetTheme(registryOptions.LoadTheme(ThemeName.DarkPlus)); textMate.SetTheme(registryOptions.LoadTheme(ThemeName.DarkPlus));
editor.Options.ShowBoxForControlCharacters = false;
} }
private static IRawTheme GetThemeFromStream(Stream stream) private static IRawTheme GetThemeFromStream(Stream stream)

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

@ -662,6 +662,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Auto Completion.
/// </summary>
public static string Label_AutoCompletion {
get {
return ResourceManager.GetString("Label_AutoCompletion", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Automatically scroll to end of console output. /// Looks up a localized string similar to Automatically scroll to end of console output.
/// </summary> /// </summary>
@ -797,6 +806,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Replace underscores with spaces when inserting completions.
/// </summary>
public static string Label_CompletionReplaceUnderscoresWithSpaces {
get {
return ResourceManager.GetString("Label_CompletionReplaceUnderscoresWithSpaces", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Confirm Delete. /// Looks up a localized string similar to Confirm Delete.
/// </summary> /// </summary>
@ -1112,6 +1130,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to General.
/// </summary>
public static string Label_General {
get {
return ResourceManager.GetString("Label_General", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Height. /// Looks up a localized string similar to Height.
/// </summary> /// </summary>
@ -1130,6 +1157,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Image Viewer.
/// </summary>
public static string Label_ImageViewer {
get {
return ResourceManager.GetString("Label_ImageViewer", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Import with Metadata. /// Looks up a localized string similar to Import with Metadata.
/// </summary> /// </summary>
@ -1166,6 +1202,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Inference.
/// </summary>
public static string Label_Inference {
get {
return ResourceManager.GetString("Label_Inference", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Inner exception. /// Looks up a localized string similar to Inner exception.
/// </summary> /// </summary>
@ -1436,6 +1481,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Output Image Files.
/// </summary>
public static string Label_OutputImageFiles {
get {
return ResourceManager.GetString("Label_OutputImageFiles", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Output Browser. /// Looks up a localized string similar to Output Browser.
/// </summary> /// </summary>
@ -1562,6 +1616,42 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Prompt.
/// </summary>
public static string Label_Prompt {
get {
return ResourceManager.GetString("Label_Prompt", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Prompt Tags.
/// </summary>
public static string Label_PromptTags {
get {
return ResourceManager.GetString("Label_PromptTags", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tags file to use for suggesting completions (Supports the a1111-sd-webui-tagcomplete .csv format).
/// </summary>
public static string Label_PromptTagsDescription {
get {
return ResourceManager.GetString("Label_PromptTagsDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Import Prompt tags.
/// </summary>
public static string Label_PromptTagsImport {
get {
return ResourceManager.GetString("Label_PromptTagsImport", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Python Packages. /// Looks up a localized string similar to Python Packages.
/// </summary> /// </summary>
@ -1814,6 +1904,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to System Information.
/// </summary>
public static string Label_SystemInformation {
get {
return ResourceManager.GetString("Label_SystemInformation", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Text to Image. /// Looks up a localized string similar to Text to Image.
/// </summary> /// </summary>

33
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -825,4 +825,37 @@
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve"> <data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>Additional folders such as IPAdapters and TextualInversions (embeddings) can be enabled here</value> <value>Additional folders such as IPAdapters and TextualInversions (embeddings) can be enabled here</value>
</data> </data>
<data name="Label_General" xml:space="preserve">
<value>General</value><comment>A general settings category</comment>
</data>
<data name="Label_Inference" xml:space="preserve">
<value>Inference</value><comment>The Inference feature page</comment>
</data>
<data name="Label_Prompt" xml:space="preserve">
<value>Prompt</value><comment>A settings category for Inference generation prompts</comment>
</data>
<data name="Label_OutputImageFiles" xml:space="preserve">
<value>Output Image Files</value>
</data>
<data name="Label_ImageViewer" xml:space="preserve">
<value>Image Viewer</value>
</data>
<data name="Label_AutoCompletion" xml:space="preserve">
<value>Auto Completion</value>
</data>
<data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve">
<value>Replace underscores with spaces when inserting completions</value>
</data>
<data name="Label_PromptTags" xml:space="preserve">
<value>Prompt Tags</value><comment>Tags for image generation prompts</comment>
</data>
<data name="Label_PromptTagsImport" xml:space="preserve">
<value>Import Prompt tags</value>
</data>
<data name="Label_PromptTagsDescription" xml:space="preserve">
<value>Tags file to use for suggesting completions (Supports the a1111-sd-webui-tagcomplete .csv format)</value>
</data>
<data name="Label_SystemInformation" xml:space="preserve">
<value>System Information</value>
</data>
</root> </root>

55
StabilityMatrix.Avalonia/Program.cs

@ -24,6 +24,7 @@ using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Updater; using StabilityMatrix.Core.Updater;
namespace StabilityMatrix.Avalonia; namespace StabilityMatrix.Avalonia;
@ -53,7 +54,8 @@ public static class Program
SetDebugBuild(); SetDebugBuild();
var parseResult = Parser.Default var parseResult = Parser
.Default
.ParseArguments<AppArgs>(args) .ParseArguments<AppArgs>(args)
.WithNotParsed(errors => .WithNotParsed(errors =>
{ {
@ -68,6 +70,7 @@ public static class Program
if (Args.HomeDirectoryOverride is { } homeDir) if (Args.HomeDirectoryOverride is { } homeDir)
{ {
Compat.SetAppDataHome(homeDir); Compat.SetAppDataHome(homeDir);
GlobalConfig.HomeDir = homeDir;
} }
// Launched for custom URI scheme, handle and exit // Launched for custom URI scheme, handle and exit
@ -77,11 +80,7 @@ public static class Program
{ {
if ( if (
Uri.TryCreate(uriArg, UriKind.Absolute, out var uri) Uri.TryCreate(uriArg, UriKind.Absolute, out var uri)
&& string.Equals( && string.Equals(uri.Scheme, UriHandler.Scheme, StringComparison.OrdinalIgnoreCase)
uri.Scheme,
UriHandler.Scheme,
StringComparison.OrdinalIgnoreCase
)
) )
{ {
UriHandler.SendAndExit(uri); UriHandler.SendAndExit(uri);
@ -147,20 +146,10 @@ public static class Program
if (Args.DisableGpuRendering) if (Args.DisableGpuRendering)
{ {
app = app.With( app = app.With(new Win32PlatformOptions { RenderingMode = new[] { Win32RenderingMode.Software } })
new Win32PlatformOptions .With(new X11PlatformOptions { RenderingMode = new[] { X11RenderingMode.Software } })
{
RenderingMode = new[] { Win32RenderingMode.Software }
}
)
.With( .With(
new X11PlatformOptions { RenderingMode = new[] { X11RenderingMode.Software } } new AvaloniaNativePlatformOptions { RenderingMode = new[] { AvaloniaNativeRenderingMode.Software } }
)
.With(
new AvaloniaNativePlatformOptions
{
RenderingMode = new[] { AvaloniaNativeRenderingMode.Software }
}
); );
} }
@ -257,10 +246,7 @@ public static class Program
try try
{ {
var process = Process.GetProcessById(pid); var process = Process.GetProcessById(pid);
process process.WaitForExitAsync(new CancellationTokenSource(timeout).Token).GetAwaiter().GetResult();
.WaitForExitAsync(new CancellationTokenSource(timeout).Token)
.GetAwaiter()
.GetResult();
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@ -281,8 +267,7 @@ public static class Program
{ {
SentrySdk.Init(o => SentrySdk.Init(o =>
{ {
o.Dsn = o.Dsn = "https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328";
"https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328";
o.StackTraceMode = StackTraceMode.Enhanced; o.StackTraceMode = StackTraceMode.Enhanced;
o.TracesSampleRate = 1.0; o.TracesSampleRate = 1.0;
o.IsGlobalModeEnabled = true; o.IsGlobalModeEnabled = true;
@ -309,10 +294,7 @@ public static class Program
}); });
} }
private static void TaskScheduler_UnobservedTaskException( private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
object? sender,
UnobservedTaskExceptionEventArgs e
)
{ {
if (e.Exception is Exception ex) if (e.Exception is Exception ex)
{ {
@ -320,10 +302,7 @@ public static class Program
} }
} }
private static void CurrentDomain_UnhandledException( private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
object sender,
UnhandledExceptionEventArgs e
)
{ {
if (e.ExceptionObject is not Exception ex) if (e.ExceptionObject is not Exception ex)
return; return;
@ -340,15 +319,9 @@ public static class Program
Logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message); Logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message);
} }
if ( if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
Application.Current?.ApplicationLifetime
is IClassicDesktopStyleApplicationLifetime lifetime
)
{
var dialog = new ExceptionDialog
{ {
DataContext = new ExceptionViewModel { Exception = ex } var dialog = new ExceptionDialog { DataContext = new ExceptionViewModel { Exception = ex } };
};
var mainWindow = lifetime.MainWindow; var mainWindow = lifetime.MainWindow;
// We can only show dialog if main window exists, and is visible // We can only show dialog if main window exists, and is visible

2
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -8,7 +8,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>./Assets/Icon.ico</ApplicationIcon> <ApplicationIcon>./Assets/Icon.ico</ApplicationIcon>
<Version>2.7.0-dev.4</Version> <Version>2.7.0-pre.999</Version>
<InformationalVersion>$(Version)</InformationalVersion> <InformationalVersion>$(Version)</InformationalVersion>
<EnableWindowsTargeting>true</EnableWindowsTargeting> <EnableWindowsTargeting>true</EnableWindowsTargeting>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

46
StabilityMatrix.Avalonia/Styles/ProgressRing.axaml

@ -7,10 +7,13 @@
<Design.PreviewWith> <Design.PreviewWith>
<StackPanel> <StackPanel>
<Border Padding="20"> <Border Padding="20">
<controls:ProgressRing StartAngle="90" EndAngle="270" Value="50" Foreground="Red" BorderThickness="5" Width="100" Height="100" /> <controls:ProgressRing Value="50" />
</Border> </Border>
<Border Padding="20"> <Border Padding="20">
<controls:ProgressRing IsIndeterminate="True" BorderThickness="10" Width="100" Height="100" /> <controls:ProgressRing StartAngle="90" EndAngle="450" Value="50" Foreground="Red" StrokeThickness="3" Width="100" Height="100" />
</Border>
<Border Padding="20">
<controls:ProgressRing IsIndeterminate="True" StrokeThickness="10" Width="100" Height="100" />
</Border> </Border>
</StackPanel> </StackPanel>
</Design.PreviewWith> </Design.PreviewWith>
@ -22,27 +25,40 @@
<Style Selector="controls|ProgressRing"> <Style Selector="controls|ProgressRing">
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColor}"/> <Setter Property="Foreground" Value="{DynamicResource SystemAccentColor}"/>
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" /> <Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
<Setter Property="BorderThickness" Value="9" /> <Setter Property="StrokeThickness" Value="5" />
<Setter Property="MinHeight" Value="16" /> <Setter Property="MinHeight" Value="16" />
<Setter Property="MinWidth" Value="16" /> <Setter Property="MinWidth" Value="16" />
<Setter Property="Transitions">
<Transitions>
<DoubleTransition
Property="SweepAngle"
Duration="0:0:0.267">
<DoubleTransition.Easing>
<LinearEasing/>
</DoubleTransition.Easing>
</DoubleTransition>
</Transitions>
</Setter>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Panel x:Name="FluentRingRoot"> <Panel x:Name="PART_RingRoot">
<Ellipse x:Name="Track" <Ellipse x:Name="PART_Track"
Stroke="{TemplateBinding Background}" Stroke="{TemplateBinding Background}"
StrokeThickness="{Binding BorderThickness.Left, RelativeSource={RelativeSource Mode=TemplatedParent}}" /> StrokeThickness="{TemplateBinding StrokeThickness}" />
<Arc x:Name="Fill" <Arc x:Name="PART_Fill"
Stroke="{TemplateBinding Foreground}" Stroke="{TemplateBinding Foreground}"
StrokeThickness="{Binding BorderThickness.Left, RelativeSource={RelativeSource Mode=TemplatedParent}}" StrokeThickness="{TemplateBinding StrokeThickness}"
StrokeLineCap="Round" /> StrokeLineCap="Round" />
</Panel> </Panel>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>
<Style Selector="controls|ProgressRing:not(:indeterminate) /template/ Arc#Fill"> <Style Selector="controls|ProgressRing:not(:indeterminate) /template/ Arc#PART_Fill">
<Setter Property="StartAngle" Value="{Binding StartAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/> <!--<Setter Property="StartAngle" Value="{Binding StartAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/>-->
<Setter Property="SweepAngle" Value="{Binding ValueAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/> <!--<Setter Property="SweepAngle" Value="{Binding SweepAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/>-->
</Style> </Style>
<Style Selector="controls|ProgressRing:preserveaspect"> <Style Selector="controls|ProgressRing:preserveaspect">
@ -50,14 +66,14 @@
<Setter Property="MinHeight" Value="32" /> <Setter Property="MinHeight" Value="32" />
</Style> </Style>
<Style Selector="controls|ProgressRing:preserveaspect /template/ Panel#FluentRingRoot"> <Style Selector="controls|ProgressRing:preserveaspect /template/ Panel#PART_RingRoot">
<Setter Property="Width" Value="{TemplateBinding Bounds, Converter={StaticResource FitSquarelyWithinAspectRatioConverter}}"/> <Setter Property="Width" Value="{TemplateBinding Bounds, Converter={StaticResource FitSquarelyWithinAspectRatioConverter}}"/>
<Setter Property="Height" Value="{Binding Width, RelativeSource={RelativeSource Mode=Self}}"/> <Setter Property="Height" Value="{Binding Width, RelativeSource={RelativeSource Mode=Self}}"/>
</Style> </Style>
<Style Selector="controls|ProgressRing[IsEnabled=True]:indeterminate /template/ Arc#Fill"> <Style Selector="controls|ProgressRing[IsEnabled=True]:indeterminate /template/ Arc#PART_Fill">
<Style.Animations> <Style.Animations>
<Animation Duration="0:0:5" Easing="LinearEasing" IterationCount="INFINITE" FillMode="Both"> <Animation Duration="0:0:4.567" Easing="LinearEasing" IterationCount="Infinite" FillMode="Both">
<KeyFrame Cue="0%"> <KeyFrame Cue="0%">
<Setter Property="StartAngle" Value="-720"/> <Setter Property="StartAngle" Value="-720"/>
<Setter Property="SweepAngle" Value="0"/> <Setter Property="SweepAngle" Value="0"/>
@ -98,7 +114,7 @@
</Style.Animations> </Style.Animations>
</Style> </Style>
<Style Selector="controls|ProgressRing[IsEnabled=True] /template/ Ellipse#Track"> <Style Selector="controls|ProgressRing[IsEnabled=True]:not(:indeterminate) /template/ Ellipse#PART_Track">
<Style.Animations> <Style.Animations>
<Animation Duration="0:0:1" IterationCount="INFINITE"> <Animation Duration="0:0:1" IterationCount="INFINITE">
<KeyFrame Cue="0%"> <KeyFrame Cue="0%">

45
StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs

@ -54,8 +54,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
/// <summary> /// <summary>
/// Gets a cancellation token using the cursor lock timeout /// Gets a cancellation token using the cursor lock timeout
/// </summary> /// </summary>
private CancellationToken WriteCursorLockTimeoutToken => private CancellationToken WriteCursorLockTimeoutToken => new CancellationTokenSource(WriteCursorLockTimeout).Token;
new CancellationTokenSource(WriteCursorLockTimeout).Token;
/// <summary> /// <summary>
/// Event invoked when an ApcMessage of type Input is received. /// Event invoked when an ApcMessage of type Input is received.
@ -132,9 +131,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
{ {
using (await writeCursorLock.LockAsync(WriteCursorLockTimeoutToken)) using (await writeCursorLock.LockAsync(WriteCursorLockTimeoutToken))
{ {
Logger.ConditionalTrace( Logger.ConditionalTrace($"Reset cursor to end: ({writeCursor} -> {Document.TextLength})");
$"Reset cursor to end: ({writeCursor} -> {Document.TextLength})"
);
writeCursor = Document.TextLength; writeCursor = Document.TextLength;
} }
DebugPrintDocument(); DebugPrintDocument();
@ -146,9 +143,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
Dispatcher.UIThread.VerifyAccess(); Dispatcher.UIThread.VerifyAccess();
// Get cancellation token // Get cancellation token
var ct = var ct = updateCts?.Token ?? throw new InvalidOperationException("Update cancellation token must be set");
updateCts?.Token
?? throw new InvalidOperationException("Update cancellation token must be set");
try try
{ {
@ -176,9 +171,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
); );
// Link the cancellation token to the write cursor lock timeout // Link the cancellation token to the write cursor lock timeout
var linkedCt = CancellationTokenSource var linkedCt = CancellationTokenSource.CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken).Token;
.CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken)
.Token;
using (await writeCursorLock.LockAsync(linkedCt)) using (await writeCursorLock.LockAsync(linkedCt))
{ {
@ -193,10 +186,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
catch (Exception e) catch (Exception e)
{ {
// Log other errors and continue here to not crash the UI thread // Log other errors and continue here to not crash the UI thread
Logger.Error( Logger.Error(e, $"Unexpected error in console update loop: {e.GetType().Name} {e.Message}");
e,
$"Unexpected error in console update loop: {e.GetType().Name} {e.Message}"
);
} }
} }
@ -248,8 +238,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
Document.Remove(lineStartOffset, lineLength); Document.Remove(lineStartOffset, lineLength);
Logger.ConditionalTrace( Logger.ConditionalTrace(
$"Moving cursor to start for carriage return " $"Moving cursor to start for carriage return " + $"({writeCursor} -> {lineStartOffset})"
+ $"({writeCursor} -> {lineStartOffset})"
); );
writeCursor = lineStartOffset; writeCursor = lineStartOffset;
} }
@ -275,10 +264,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
else else
{ {
// We want to move up one line // We want to move up one line
var targetLocation = new TextLocation( var targetLocation = new TextLocation(currentLocation.Line - 1, currentLocation.Column);
currentLocation.Line - 1,
currentLocation.Column
);
var targetOffset = Document.GetOffset(targetLocation); var targetOffset = Document.GetOffset(targetLocation);
// Update cursor to target offset // Update cursor to target offset
@ -297,13 +283,17 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
{ {
// Get the current line, we'll insert spaces from start to end // Get the current line, we'll insert spaces from start to end
var currentLine = Document.GetLineByOffset(writeCursor); var currentLine = Document.GetLineByOffset(writeCursor);
// Must be smaller than total lines
currentLine =
currentLine.LineNumber < Document.LineCount
? currentLine
: Document.GetLineByNumber(Document.LineCount - 1);
// Make some spaces to insert // Make some spaces to insert
var spaces = new string(' ', currentLine.Length); var spaces = new string(' ', currentLine.Length);
// Insert the text // Insert the text
Logger.ConditionalTrace( Logger.ConditionalTrace($"Erasing line {currentLine.LineNumber}: (length = {currentLine.Length})");
$"Erasing line {currentLine.LineNumber}: (length = {currentLine.Length})"
);
using (Document.RunUpdate()) using (Document.RunUpdate())
{ {
Document.Replace(currentLine.Offset, currentLine.Length, spaces); Document.Replace(currentLine.Offset, currentLine.Length, spaces);
@ -346,8 +336,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
{ {
var nextLine = Document.GetLineByNumber(currentLine.LineNumber + 1); var nextLine = Document.GetLineByNumber(currentLine.LineNumber + 1);
Logger.ConditionalTrace( Logger.ConditionalTrace(
$"Moving cursor to start of next line " $"Moving cursor to start of next line " + $"({writeCursor} -> {nextLine.Offset})"
+ $"({writeCursor} -> {nextLine.Offset})"
); );
writeCursor = nextLine.Offset; writeCursor = nextLine.Offset;
} }
@ -393,9 +382,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
if (remainingLength > 0) if (remainingLength > 0)
{ {
var textToInsert = text[replaceLength..]; var textToInsert = text[replaceLength..];
Logger.ConditionalTrace( Logger.ConditionalTrace($"Inserting: (cursor = {writeCursor}, " + $"text = {textToInsert.ToRepr()})");
$"Inserting: (cursor = {writeCursor}, " + $"text = {textToInsert.ToRepr()})"
);
Document.Insert(writeCursor, textToInsert); Document.Insert(writeCursor, textToInsert);
writeCursor += textToInsert.Length; writeCursor += textToInsert.Length;

8
StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs

@ -7,6 +7,7 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.HardwareInfo;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels;
@ -25,8 +26,7 @@ public partial class FirstLaunchSetupViewModel : ViewModelBase
private RefreshBadgeViewModel checkHardwareBadge = private RefreshBadgeViewModel checkHardwareBadge =
new() new()
{ {
WorkingToolTipText = WorkingToolTipText = "We're checking some hardware specifications to determine compatibility.",
"We're checking some hardware specifications to determine compatibility.",
SuccessToolTipText = "Everything looks good!", SuccessToolTipText = "Everything looks good!",
FailToolTipText = FailToolTipText =
"We recommend a GPU with CUDA support for the best experience. " "We recommend a GPU with CUDA support for the best experience. "
@ -48,9 +48,7 @@ public partial class FirstLaunchSetupViewModel : ViewModelBase
gpuInfo = await Task.Run(() => HardwareHelper.IterGpuInfo().ToArray()); gpuInfo = await Task.Run(() => HardwareHelper.IterGpuInfo().ToArray());
} }
// First Nvidia GPU // First Nvidia GPU
var activeGpu = gpuInfo.FirstOrDefault( var activeGpu = gpuInfo.FirstOrDefault(gpu => gpu.Name?.ToLowerInvariant().Contains("nvidia") ?? false);
gpu => gpu.Name?.ToLowerInvariant().Contains("nvidia") ?? false
);
var isNvidia = activeGpu is not null; var isNvidia = activeGpu is not null;
// Otherwise first GPU // Otherwise first GPU
activeGpu ??= gpuInfo.FirstOrDefault(); activeGpu ??= gpuInfo.FirstOrDefault();

208
StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs

@ -1,7 +1,28 @@
using FluentAvalonia.UI.Controls; using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using NLog;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Settings; using StabilityMatrix.Avalonia.Views.Settings;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol; using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -9,12 +30,191 @@ namespace StabilityMatrix.Avalonia.ViewModels.Settings;
[View(typeof(InferenceSettingsPage))] [View(typeof(InferenceSettingsPage))]
[Singleton, ManagedService] [Singleton, ManagedService]
public class InferenceSettingsViewModel : PageViewModelBase public partial class InferenceSettingsViewModel : PageViewModelBase
{ {
private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager;
private readonly ICompletionProvider completionProvider;
/// <inheritdoc /> /// <inheritdoc />
public override string Title => "Inference"; public override string Title => "Inference";
/// <inheritdoc /> /// <inheritdoc />
public override IconSource IconSource => public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
[ObservableProperty]
private bool isPromptCompletionEnabled = true;
[ObservableProperty]
private IReadOnlyList<string> availableTagCompletionCsvs = Array.Empty<string>();
[ObservableProperty]
private string? selectedTagCompletionCsv;
[ObservableProperty]
private bool isCompletionRemoveUnderscoresEnabled = true;
[ObservableProperty]
[CustomValidation(typeof(InferenceSettingsViewModel), nameof(ValidateOutputImageFileNameFormat))]
private string? outputImageFileNameFormat;
[ObservableProperty]
private string? outputImageFileNameFormatSample;
public IEnumerable<FileNameFormatVar> OutputImageFileNameFormatVars =>
FileNameFormatProvider
.GetSample()
.Substitutions
.Select(kv => new FileNameFormatVar { Variable = $"{{{kv.Key}}}", Example = kv.Value.Invoke() });
[ObservableProperty]
private bool isImageViewerPixelGridEnabled = true;
public InferenceSettingsViewModel(
INotificationService notificationService,
IPrerequisiteHelper prerequisiteHelper,
IPyRunner pyRunner,
ServiceManager<ViewModelBase> dialogFactory,
ICompletionProvider completionProvider,
ITrackedDownloadService trackedDownloadService,
IModelIndexService modelIndexService,
INavigationService<SettingsViewModel> settingsNavigationService,
IAccountsService accountsService,
ISettingsManager settingsManager
)
{
this.settingsManager = settingsManager;
this.notificationService = notificationService;
this.completionProvider = completionProvider;
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedTagCompletionCsv,
settings => settings.TagCompletionCsv
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsPromptCompletionEnabled,
settings => settings.IsPromptCompletionEnabled,
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsCompletionRemoveUnderscoresEnabled,
settings => settings.IsCompletionRemoveUnderscoresEnabled,
true
);
this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat)
.Throttle(TimeSpan.FromMilliseconds(50))
.Subscribe(formatProperty =>
{
var provider = FileNameFormatProvider.GetSample();
var template = formatProperty.Value ?? string.Empty;
if (!string.IsNullOrEmpty(template) && provider.Validate(template) == ValidationResult.Success)
{
var format = FileNameFormat.Parse(template, provider);
OutputImageFileNameFormatSample = format.GetFileName() + ".png";
}
else
{
// Use default format if empty
var defaultFormat = FileNameFormat.Parse(FileNameFormat.DefaultTemplate, provider);
OutputImageFileNameFormatSample = defaultFormat.GetFileName() + ".png";
}
});
settingsManager.RelayPropertyFor(
this,
vm => vm.OutputImageFileNameFormat,
settings => settings.InferenceOutputImageFileNameFormat,
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsImageViewerPixelGridEnabled,
settings => settings.IsImageViewerPixelGridEnabled,
true
);
ImportTagCsvCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
}
/// <summary>
/// Validator for <see cref="OutputImageFileNameFormat"/>
/// </summary>
public static ValidationResult ValidateOutputImageFileNameFormat(string? format, ValidationContext context)
{
return FileNameFormatProvider.GetSample().Validate(format ?? string.Empty);
}
/// <inheritdoc />
public override void OnLoaded()
{
base.OnLoaded();
UpdateAvailableTagCompletionCsvs();
}
#region Commands
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task ImportTagCsv()
{
var storage = App.StorageProvider;
var files = await storage.OpenFilePickerAsync(
new FilePickerOpenOptions
{
FileTypeFilter = new List<FilePickerFileType> { new("CSV") { Patterns = ["*.csv"] } }
}
);
if (files.Count == 0)
return;
var sourceFile = new FilePath(files[0].TryGetLocalPath()!);
var tagsDir = settingsManager.TagsDirectory;
tagsDir.Create();
// Copy to tags directory
var targetFile = tagsDir.JoinFile(sourceFile.Name);
await sourceFile.CopyToAsync(targetFile);
// Update index
UpdateAvailableTagCompletionCsvs();
// Trigger load
completionProvider.BackgroundLoadFromFile(targetFile, true);
notificationService.Show(
$"Imported {sourceFile.Name}",
$"The {sourceFile.Name} file has been imported.",
NotificationType.Success
);
}
#endregion
private void UpdateAvailableTagCompletionCsvs()
{
if (!settingsManager.IsLibraryDirSet)
return;
if (settingsManager.TagsDirectory is not { Exists: true } tagsDir)
return;
var csvFiles = tagsDir.Info.EnumerateFiles("*.csv");
AvailableTagCompletionCsvs = csvFiles.Select(f => f.Name).ToImmutableArray();
// Set selected to current if exists
var settingsCsv = settingsManager.Settings.TagCompletionCsv;
if (settingsCsv is not null && AvailableTagCompletionCsvs.Contains(settingsCsv))
{
SelectedTagCompletionCsv = settingsCsv;
}
}
} }

244
StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs

@ -44,6 +44,7 @@ using StabilityMatrix.Avalonia.Views.Settings;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
@ -73,8 +74,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
public SharedState SharedState { get; } public SharedState SharedState { get; }
public override string Title => "Settings"; public override string Title => "Settings";
public override IconSource IconSource => public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
// ReSharper disable once MemberCanBeMadeStatic.Global // ReSharper disable once MemberCanBeMadeStatic.Global
public string AppVersion => public string AppVersion =>
@ -102,54 +102,35 @@ public partial class MainSettingsViewModel : PageViewModelBase
[ObservableProperty] [ObservableProperty]
private bool removeSymlinksOnShutdown; private bool removeSymlinksOnShutdown;
// Inference UI section // Integrations section
[ObservableProperty]
private bool isPromptCompletionEnabled = true;
[ObservableProperty] [ObservableProperty]
private IReadOnlyList<string> availableTagCompletionCsvs = Array.Empty<string>(); private bool isDiscordRichPresenceEnabled;
// Debug section
[ObservableProperty] [ObservableProperty]
private string? selectedTagCompletionCsv; private string? debugPaths;
[ObservableProperty] [ObservableProperty]
private bool isCompletionRemoveUnderscoresEnabled = true; private string? debugCompatInfo;
[ObservableProperty] [ObservableProperty]
[CustomValidation(typeof(MainSettingsViewModel), nameof(ValidateOutputImageFileNameFormat))] private string? debugGpuInfo;
private string? outputImageFileNameFormat;
[ObservableProperty] #region System Info
private string? outputImageFileNameFormatSample;
public IEnumerable<FileNameFormatVar> OutputImageFileNameFormatVars => private static Lazy<IReadOnlyList<GpuInfo>> GpuInfosLazy { get; } =
FileNameFormatProvider new(() => HardwareHelper.IterGpuInfo().ToImmutableArray());
.GetSample()
.Substitutions.Select(
kv =>
new FileNameFormatVar
{
Variable = $"{{{kv.Key}}}",
Example = kv.Value.Invoke()
}
);
[ObservableProperty] public static IReadOnlyList<GpuInfo> GpuInfos => GpuInfosLazy.Value;
private bool isImageViewerPixelGridEnabled = true;
// Integrations section
[ObservableProperty] [ObservableProperty]
private bool isDiscordRichPresenceEnabled; private MemoryInfo memoryInfo;
// Debug section private readonly DispatcherTimer hardwareInfoUpdateTimer = new() { Interval = TimeSpan.FromSeconds(2.627) };
[ObservableProperty]
private string? debugPaths;
[ObservableProperty] public Task<CpuInfo> CpuInfoAsync => HardwareHelper.GetCpuInfoAsync();
private string? debugCompatInfo;
[ObservableProperty] #endregion
private string? debugGpuInfo;
// Info section // Info section
private const int VersionTapCountThreshold = 7; private const int VersionTapCountThreshold = 7;
@ -162,8 +143,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
public string VersionFlyoutText => public string VersionFlyoutText =>
$"You are {VersionTapCountThreshold - VersionTapCount} clicks away from enabling Debug options."; $"You are {VersionTapCountThreshold - VersionTapCount} clicks away from enabling Debug options.";
public string DataDirectory => public string DataDirectory => settingsManager.IsLibraryDirSet ? settingsManager.LibraryDir : "Not set";
settingsManager.IsLibraryDirSet ? settingsManager.LibraryDir : "Not set";
public MainSettingsViewModel( public MainSettingsViewModel(
INotificationService notificationService, INotificationService notificationService,
@ -211,77 +191,28 @@ public partial class MainSettingsViewModel : PageViewModelBase
true true
); );
settingsManager.RelayPropertyFor( settingsManager.RelayPropertyFor(this, vm => vm.SelectedAnimationScale, settings => settings.AnimationScale);
this,
vm => vm.SelectedAnimationScale,
settings => settings.AnimationScale
);
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedTagCompletionCsv,
settings => settings.TagCompletionCsv
);
settingsManager.RelayPropertyFor( DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
this,
vm => vm.IsPromptCompletionEnabled,
settings => settings.IsPromptCompletionEnabled,
true
);
settingsManager.RelayPropertyFor( hardwareInfoUpdateTimer.Tick += OnHardwareInfoUpdateTimerTick;
this, }
vm => vm.IsCompletionRemoveUnderscoresEnabled,
settings => settings.IsCompletionRemoveUnderscoresEnabled,
true
);
this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat) /// <inheritdoc />
.Throttle(TimeSpan.FromMilliseconds(50)) public override void OnLoaded()
.Subscribe(formatProperty =>
{ {
var provider = FileNameFormatProvider.GetSample(); base.OnLoaded();
var template = formatProperty.Value ?? string.Empty;
if ( hardwareInfoUpdateTimer.Start();
!string.IsNullOrEmpty(template) OnHardwareInfoUpdateTimerTick(null, null!);
&& provider.Validate(template) == ValidationResult.Success
)
{
var format = FileNameFormat.Parse(template, provider);
OutputImageFileNameFormatSample = format.GetFileName() + ".png";
}
else
{
// Use default format if empty
var defaultFormat = FileNameFormat.Parse(
FileNameFormat.DefaultTemplate,
provider
);
OutputImageFileNameFormatSample = defaultFormat.GetFileName() + ".png";
} }
});
settingsManager.RelayPropertyFor(
this,
vm => vm.OutputImageFileNameFormat,
settings => settings.InferenceOutputImageFileNameFormat,
true
);
settingsManager.RelayPropertyFor( /// <inheritdoc />
this, public override void OnUnloaded()
vm => vm.IsImageViewerPixelGridEnabled, {
settings => settings.IsImageViewerPixelGridEnabled, base.OnUnloaded();
true
);
DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler( hardwareInfoUpdateTimer.Stop();
notificationService,
LogLevel.Warn
);
ImportTagCsvCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -291,18 +222,13 @@ public partial class MainSettingsViewModel : PageViewModelBase
await notificationService.TryAsync(completionProvider.Setup()); await notificationService.TryAsync(completionProvider.Setup());
UpdateAvailableTagCompletionCsvs();
// Start accounts update // Start accounts update
accountsService.RefreshAsync().SafeFireAndForget(); accountsService.RefreshAsync().SafeFireAndForget();
} }
public static ValidationResult ValidateOutputImageFileNameFormat( private void OnHardwareInfoUpdateTimerTick(object? sender, EventArgs e)
string? format,
ValidationContext context
)
{ {
return FileNameFormatProvider.GetSample().Validate(format ?? string.Empty); MemoryInfo = HardwareHelper.GetMemoryInfo();
} }
partial void OnSelectedThemeChanged(string? value) partial void OnSelectedThemeChanged(string? value)
@ -341,7 +267,9 @@ public partial class MainSettingsViewModel : PageViewModelBase
CloseButtonText = Resources.Action_RelaunchLater CloseButtonText = Resources.Action_RelaunchLater
}; };
Dispatcher.UIThread.InvokeAsync(async () => Dispatcher
.UIThread
.InvokeAsync(async () =>
{ {
if (await dialog.ShowAsync() == ContentDialogResult.Primary) if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{ {
@ -352,11 +280,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
} }
else else
{ {
Logger.Info( Logger.Info("Requested invalid language change from {Old} to {New}", oldValue, newValue);
"Requested invalid language change from {Old} to {New}",
oldValue,
newValue
);
} }
} }
@ -379,7 +303,9 @@ public partial class MainSettingsViewModel : PageViewModelBase
[RelayCommand] [RelayCommand]
private void NavigateToSubPage(Type viewModelType) private void NavigateToSubPage(Type viewModelType)
{ {
Dispatcher.UIThread.Post( Dispatcher
.UIThread
.Post(
() => () =>
settingsNavigationService.NavigateTo( settingsNavigationService.NavigateTo(
viewModelType, viewModelType,
@ -397,8 +323,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
var viewModel = dialogFactory.Get<EnvVarsViewModel>(); var viewModel = dialogFactory.Get<EnvVarsViewModel>();
// Load current settings // Load current settings
var current = var current = settingsManager.Settings.EnvironmentVariables ?? new Dictionary<string, string>();
settingsManager.Settings.EnvironmentVariables ?? new Dictionary<string, string>();
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>( viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>(
current.Select(kvp => new EnvVarKeyPair(kvp.Key, kvp.Value)) current.Select(kvp => new EnvVarKeyPair(kvp.Key, kvp.Value))
); );
@ -414,7 +339,8 @@ public partial class MainSettingsViewModel : PageViewModelBase
if (await dialog.ShowAsync() == ContentDialogResult.Primary) if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{ {
// Save settings // Save settings
var newEnvVars = viewModel.EnvVars var newEnvVars = viewModel
.EnvVars
.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key)) .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key))
.GroupBy(kvp => kvp.Key, StringComparer.Ordinal) .GroupBy(kvp => kvp.Key, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.First().Value, StringComparer.Ordinal); .ToDictionary(g => g.Key, g => g.First().Value, StringComparer.Ordinal);
@ -453,68 +379,6 @@ public partial class MainSettingsViewModel : PageViewModelBase
#endregion #endregion
#region Inference UI
private void UpdateAvailableTagCompletionCsvs()
{
if (!settingsManager.IsLibraryDirSet)
return;
var tagsDir = settingsManager.TagsDirectory;
if (!tagsDir.Exists)
return;
var csvFiles = tagsDir.Info.EnumerateFiles("*.csv");
AvailableTagCompletionCsvs = csvFiles.Select(f => f.Name).ToImmutableArray();
// Set selected to current if exists
var settingsCsv = settingsManager.Settings.TagCompletionCsv;
if (settingsCsv is not null && AvailableTagCompletionCsvs.Contains(settingsCsv))
{
SelectedTagCompletionCsv = settingsCsv;
}
}
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task ImportTagCsv()
{
var storage = App.StorageProvider;
var files = await storage.OpenFilePickerAsync(
new FilePickerOpenOptions
{
FileTypeFilter = new List<FilePickerFileType>
{
new("CSV") { Patterns = new[] { "*.csv" }, }
}
}
);
if (files.Count == 0)
return;
var sourceFile = new FilePath(files[0].TryGetLocalPath()!);
var tagsDir = settingsManager.TagsDirectory;
tagsDir.Create();
// Copy to tags directory
var targetFile = tagsDir.JoinFile(sourceFile.Name);
await sourceFile.CopyToAsync(targetFile);
// Update index
UpdateAvailableTagCompletionCsvs();
// Trigger load
completionProvider.BackgroundLoadFromFile(targetFile, true);
notificationService.Show(
$"Imported {sourceFile.Name}",
$"The {sourceFile.Name} file has been imported.",
NotificationType.Success
);
}
#endregion
#region System #region System
/// <summary> /// <summary>
@ -531,10 +395,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
await using var _ = new MinimumDelay(200, 300); await using var _ = new MinimumDelay(200, 300);
var shortcutDir = new DirectoryPath( var shortcutDir = new DirectoryPath(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs");
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),
"Programs"
);
var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk"); var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk");
var appPath = Compat.AppCurrentPath; var appPath = Compat.AppCurrentPath;
@ -566,8 +427,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
// Confirmation dialog // Confirmation dialog
var dialog = new BetterContentDialog var dialog = new BetterContentDialog
{ {
Title = Title = "This will create a shortcut for Stability Matrix in the Start Menu for all users",
"This will create a shortcut for Stability Matrix in the Start Menu for all users",
Content = "You will be prompted for administrator privileges. Continue?", Content = "You will be prompted for administrator privileges. Continue?",
PrimaryButtonText = Resources.Action_Yes, PrimaryButtonText = Resources.Action_Yes,
CloseButtonText = Resources.Action_Cancel, CloseButtonText = Resources.Action_Cancel,
@ -764,16 +624,12 @@ public partial class MainSettingsViewModel : PageViewModelBase
private async Task DebugMakeImageGrid() private async Task DebugMakeImageGrid()
{ {
var provider = App.StorageProvider; var provider = App.StorageProvider;
var files = await provider.OpenFilePickerAsync( var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions() { AllowMultiple = true });
new FilePickerOpenOptions() { AllowMultiple = true }
);
if (files.Count == 0) if (files.Count == 0)
return; return;
var images = await files.SelectAsync( var images = await files.SelectAsync(async f => SKImage.FromEncodedData(await f.OpenReadAsync()));
async f => SKImage.FromEncodedData(await f.OpenReadAsync())
);
var grid = ImageProcessor.CreateImageGrid(images.ToImmutableArray()); var grid = ImageProcessor.CreateImageGrid(images.ToImmutableArray());
@ -923,11 +779,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
} }
catch (Exception e) catch (Exception e)
{ {
notificationService.Show( notificationService.Show("Failed to read licenses information", $"{e}", NotificationType.Error);
"Failed to read licenses information",
$"{e}",
NotificationType.Error
);
} }
} }

5
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -18,13 +18,12 @@ namespace StabilityMatrix.Avalonia.ViewModels;
public partial class SettingsViewModel : PageViewModelBase public partial class SettingsViewModel : PageViewModelBase
{ {
public override string Title => "Settings"; public override string Title => "Settings";
public override IconSource IconSource => public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
public IReadOnlyList<PageViewModelBase> SubPages { get; } public IReadOnlyList<PageViewModelBase> SubPages { get; }
[ObservableProperty] [ObservableProperty]
private ObservableCollection<PageViewModelBase> currentPagePath = new(); private ObservableCollection<PageViewModelBase> currentPagePath = [];
[ObservableProperty] [ObservableProperty]
private PageViewModelBase? currentPage; private PageViewModelBase? currentPage;

6
StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml.cs

@ -34,11 +34,15 @@ public partial class PackageModificationDialog : UserControlBase
textMate.SetGrammar(scope); textMate.SetGrammar(scope);
textMate.SetTheme(options.LoadTheme(ThemeName.DarkPlus)); textMate.SetTheme(options.LoadTheme(ThemeName.DarkPlus));
editor.Options.ShowBoxForControlCharacters = false;
} }
EventManager.Instance.ScrollToBottomRequested += (_, _) => EventManager.Instance.ScrollToBottomRequested += (_, _) =>
{ {
Dispatcher.UIThread.Invoke(() => Dispatcher
.UIThread
.Invoke(() =>
{ {
var editor = this.FindControl<TextEditor>("Console"); var editor = this.FindControl<TextEditor>("Console");
if (editor?.Document == null) if (editor?.Document == null)

148
StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml

@ -4,66 +4,144 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.Models.Inference"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vmSettings="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Settings" xmlns:vmSettings="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Settings"
d:DataContext="{x:Static mocks:DesignData.InferenceSettingsViewModel}" d:DataContext="{x:Static mocks:DesignData.InferenceSettingsViewModel}"
d:DesignHeight="450" d:DesignHeight="650"
d:DesignWidth="800" d:DesignWidth="900"
x:DataType="vmSettings:InferenceSettingsViewModel" x:DataType="vmSettings:InferenceSettingsViewModel"
mc:Ignorable="d"> mc:Ignorable="d">
<controls:UserControlBase.Styles>
<Style Selector="sg|SpacedGrid &gt; ui|SettingsExpander">
<Setter Property="Margin" Value="8,0" />
</Style>
</controls:UserControlBase.Styles>
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8,16" Spacing="8"> <StackPanel Margin="8,16" Spacing="8">
<!-- Theme -->
<Grid RowDefinitions="Auto,*,*,*"> <!-- Prompt -->
<sg:SpacedGrid RowDefinitions="Auto,*" RowSpacing="4">
<TextBlock <TextBlock
Margin="0,0,0,8" Margin="0,0,0,4"
FontWeight="Medium" FontWeight="Medium"
Text="Stuff" /> Text="{x:Static lang:Resources.Label_Prompt}" />
<!-- Auto Completion -->
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="1" Grid.Row="1"
Margin="8,0,8,4" Margin="8,0"
Header="{x:Static lang:Resources.Label_Theme}" Header="{x:Static lang:Resources.Label_AutoCompletion}">
IconSource="WeatherMoon">
<ui:SettingsExpander.Footer> <ui:SettingsExpander.IconSource>
<ComboBox MinWidth="100" /> <controls:FASymbolIconSource Symbol="fa-solid fa-wand-magic-sparkles" />
</ui:SettingsExpander.Footer> </ui:SettingsExpander.IconSource>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Language}"
IconSource="Character">
<ui:SettingsExpander.Footer> <ui:SettingsExpander.Footer>
<ComboBox MinWidth="100" /> <ToggleSwitch IsChecked="{Binding IsPromptCompletionEnabled}" />
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
<!-- Tag csv selection -->
<ui:SettingsExpanderItem
Content="{x:Static lang:Resources.Label_PromptTags}"
Description="{x:Static lang:Resources.Label_PromptTagsDescription}"
IconSource="Tag"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<ui:FAComboBox ItemsSource="{Binding AvailableTagCompletionCsvs}" SelectedItem="{Binding SelectedTagCompletionCsv}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Tag csv import -->
<ui:SettingsExpanderItem
Content="{x:Static lang:Resources.Label_PromptTagsImport}"
IconSource="Add"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<Button Command="{Binding ImportTagCsvCommand}" Content="Import" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Remove underscores -->
<ui:SettingsExpanderItem
Content="{x:Static lang:Resources.Label_CompletionReplaceUnderscoresWithSpaces}"
IconSource="Underline"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<CheckBox Margin="8" IsChecked="{Binding IsCompletionRemoveUnderscoresEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander> </ui:SettingsExpander>
<ui:SettingsExpander </sg:SpacedGrid>
Grid.Row="3"
Margin="8,0,8,4"
Header="Inference"
IconSource="Code" />
</Grid>
<!-- Checkpoints Manager Options --> <!-- General -->
<Grid RowDefinitions="auto,*,Auto"> <sg:SpacedGrid RowDefinitions="Auto,*,*" RowSpacing="4">
<TextBlock <TextBlock
Margin="0,0,0,8" Margin="0,0,0,4"
FontWeight="Medium" FontWeight="Medium"
Text="Other stuff" /> Text="{x:Static lang:Resources.Label_General}" />
<!-- Image Viewer -->
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="1" Grid.Row="1"
Margin="8,0" Header="{x:Static lang:Resources.Label_ImageViewer}"
IconSource="Folder"> IconSource="Image"
<ui:SettingsExpander.Footer> IsExpanded="True">
<CheckBox Margin="8" />
</ui:SettingsExpander.Footer> <!-- Pixel grid -->
<ui:SettingsExpanderItem Content="Show pixel grid at high zoom levels" IconSource="ViewAll">
<ui:SettingsExpanderItem.Footer>
<CheckBox Margin="8" IsChecked="{Binding IsImageViewerPixelGridEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<!-- Output Image Files -->
<ui:SettingsExpander
Grid.Row="2"
Header="{x:Static lang:Resources.Label_OutputImageFiles}"
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource Symbol="TabDesktopImage" />
</ui:SettingsExpander.IconSource>
<!-- File name pattern -->
<ui:SettingsExpanderItem
Content="File name pattern"
Description="{Binding OutputImageFileNameFormatSample}"
IconSource="Rename">
<ui:SettingsExpanderItem.Footer>
<TextBox
Name="OutputImageFileNameFormatTextBox"
MinWidth="150"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
FontSize="13"
Text="{Binding OutputImageFileNameFormat}"
Watermark="{x:Static inference:FileNameFormat.DefaultTemplate}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander> </ui:SettingsExpander>
</Grid>
<ui:TeachingTip
Title="Format Variables"
Grid.Row="2"
IsOpen="{Binding #OutputImageFileNameFormatTextBox.IsFocused}"
PreferredPlacement="Top"
Target="{Binding #OutputImageFileNameFormatTextBox, Mode=OneWay}">
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding OutputImageFileNameFormatVars}" />
<!--<mdxaml:MarkdownScrollViewer
Markdown="{Binding OutputImageFileNameFormatGuideMarkdown}"/>-->
</ui:TeachingTip>
</sg:SpacedGrid>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</controls:UserControlBase> </controls:UserControlBase>

243
StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml

@ -7,6 +7,8 @@
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:hardwareInfo="clr-namespace:StabilityMatrix.Core.Helper.HardwareInfo;assembly=StabilityMatrix.Core"
xmlns:helper="clr-namespace:StabilityMatrix.Core.Helper;assembly=StabilityMatrix.Core"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.Models.Inference" xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.Models.Inference"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@ -28,10 +30,17 @@
<controls:UserControlBase.Resources> <controls:UserControlBase.Resources>
<converters:CultureInfoDisplayConverter x:Key="CultureInfoDisplayConverter" /> <converters:CultureInfoDisplayConverter x:Key="CultureInfoDisplayConverter" />
<converters:IndexPlusOneConverter x:Key="IndexPlusOneConverter" />
<converters:EnumStringConverter x:Key="EnumStringConverter" /> <converters:EnumStringConverter x:Key="EnumStringConverter" />
<converters:EnumToBooleanConverter x:Key="EnumBoolConverter" /> <converters:EnumToBooleanConverter x:Key="EnumBoolConverter" />
</controls:UserControlBase.Resources> </controls:UserControlBase.Resources>
<controls:UserControlBase.Styles>
<Style Selector="sg|SpacedGrid &gt; ui|SettingsExpander">
<Setter Property="Margin" Value="8,0" />
</Style>
</controls:UserControlBase.Styles>
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8,16" Spacing="8"> <StackPanel Margin="8,16" Spacing="8">
<!-- Theme --> <!-- Theme -->
@ -105,111 +114,29 @@
</ui:SettingsExpander> </ui:SettingsExpander>
</Grid> </Grid>
<!-- Inference UI --> <!-- General -->
<Grid Margin="0,8,0,0" RowDefinitions="auto,*,*,*"> <sg:SpacedGrid RowDefinitions="Auto,*" RowSpacing="4">
<TextBlock <TextBlock
Margin="0,0,0,8" Margin="0,0,0,4"
FontWeight="Medium" FontWeight="Medium"
Text="Inference" /> Text="{x:Static lang:Resources.Label_General}" />
<!-- Auto Completion --> <!-- Link to Inference Sub-Settings -->
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="1" Grid.Row="1"
Margin="8,0,8,4" Margin="8,0"
Header="Prompt Auto Completion"> ActionIconSource="ChevronRight"
<ui:SettingsExpander.IconSource> Command="{Binding NavigateToSubPageCommand}"
<controls:FASymbolIconSource Symbol="fa-solid fa-wand-magic-sparkles" /> CommandParameter="{x:Type vmSettings:InferenceSettingsViewModel}"
</ui:SettingsExpander.IconSource> Header="{x:Static lang:Resources.Label_Inference}"
IsClickEnabled="True">
<!-- Enable toggle -->
<ui:SettingsExpanderItem Content="Enable">
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch IsChecked="{Binding IsPromptCompletionEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Tag csv selection -->
<ui:SettingsExpanderItem
Content="Tag Source"
Description="Tags to use for completion in .csv format (Compatible with a1111-sd-webui-tagcomplete)"
IconSource="Tag"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<ui:FAComboBox ItemsSource="{Binding AvailableTagCompletionCsvs}" SelectedItem="{Binding SelectedTagCompletionCsv}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Tag csv import -->
<ui:SettingsExpanderItem
Content="Import Tag Source .csv"
IconSource="Add"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<Button Command="{Binding ImportTagCsvCommand}" Content="Import" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- Remove underscores -->
<ui:SettingsExpanderItem
Content="Replace underscores with spaces when inserting completions"
IconSource="Underline"
IsEnabled="{Binding IsPromptCompletionEnabled}">
<ui:SettingsExpanderItem.Footer>
<CheckBox Margin="8" IsChecked="{Binding IsCompletionRemoveUnderscoresEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<!-- Image Viewer -->
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0,8,4"
Header="Image Viewer"
IconSource="Image">
<!-- Pixel grid -->
<ui:SettingsExpanderItem Content="Show pixel grid at high zoom levels" IconSource="ViewAll">
<ui:SettingsExpanderItem.Footer>
<CheckBox Margin="8" IsChecked="{Binding IsImageViewerPixelGridEnabled}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<!-- Output Image Files -->
<ui:SettingsExpander
Grid.Row="3"
Margin="8,0,8,4"
Header="Output Image Files">
<ui:SettingsExpander.IconSource> <ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource Symbol="TabDesktopImage" /> <fluentIcons:SymbolIconSource
FontSize="10"
IsFilled="True"
Symbol="AppGeneric" />
</ui:SettingsExpander.IconSource> </ui:SettingsExpander.IconSource>
<!-- File name pattern -->
<ui:SettingsExpanderItem
Content="File name pattern"
Description="{Binding OutputImageFileNameFormatSample}"
IconSource="Rename">
<ui:SettingsExpanderItem.Footer>
<TextBox
Name="OutputImageFileNameFormatTextBox"
MinWidth="150"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
FontSize="13"
Text="{Binding OutputImageFileNameFormat}"
Watermark="{x:Static inference:FileNameFormat.DefaultTemplate}" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander> </ui:SettingsExpander>
</sg:SpacedGrid>
<ui:TeachingTip
Title="Format Variables"
Grid.Row="3"
IsOpen="{Binding #OutputImageFileNameFormatTextBox.IsFocused}"
PreferredPlacement="Top"
Target="{Binding #OutputImageFileNameFormatTextBox, Mode=OneWay}">
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding OutputImageFileNameFormatVars}" />
<!--<mdxaml:MarkdownScrollViewer
Markdown="{Binding OutputImageFileNameFormatGuideMarkdown}"/>-->
</ui:TeachingTip>
</Grid>
<!-- Environment Options --> <!-- Environment Options -->
<Grid RowDefinitions="Auto, Auto, Auto"> <Grid RowDefinitions="Auto, Auto, Auto">
@ -283,7 +210,7 @@
</sg:SpacedGrid> </sg:SpacedGrid>
<!-- System Options --> <!-- System Options -->
<sg:SpacedGrid RowDefinitions="Auto,Auto,Auto,Auto" RowSpacing="4"> <sg:SpacedGrid RowDefinitions="Auto,Auto,Auto,Auto,Auto" RowSpacing="4">
<TextBlock <TextBlock
Margin="0,0,0,4" Margin="0,0,0,4"
FontWeight="Medium" FontWeight="Medium"
@ -292,7 +219,6 @@
<!-- Updates page --> <!-- Updates page -->
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="1" Grid.Row="1"
Margin="8,0"
ActionIconSource="ChevronRight" ActionIconSource="ChevronRight"
Command="{Binding NavigateToSubPageCommand}" Command="{Binding NavigateToSubPageCommand}"
CommandParameter="{x:Type vmSettings:UpdateSettingsViewModel}" CommandParameter="{x:Type vmSettings:UpdateSettingsViewModel}"
@ -308,7 +234,6 @@
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="2" Grid.Row="2"
Margin="8,0"
Description="{x:Static lang:Resources.Label_AddToStartMenu_Details}" Description="{x:Static lang:Resources.Label_AddToStartMenu_Details}"
Header="{x:Static lang:Resources.Label_AddToStartMenu}" Header="{x:Static lang:Resources.Label_AddToStartMenu}"
IconSource="StarAdd" IconSource="StarAdd"
@ -348,7 +273,6 @@
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="3" Grid.Row="3"
Margin="8,0"
Description="{x:Static lang:Resources.Label_SelectNewDataDirectory_Details}" Description="{x:Static lang:Resources.Label_SelectNewDataDirectory_Details}"
IconSource="MoveToFolder"> IconSource="MoveToFolder">
<ui:SettingsExpander.Header> <ui:SettingsExpander.Header>
@ -376,6 +300,121 @@
</Button> </Button>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
<ui:SettingsExpander Grid.Row="4" Header="{x:Static lang:Resources.Label_SystemInformation}">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource
FontSize="10"
IsFilled="True"
Symbol="Info" />
</ui:SettingsExpander.IconSource>
<!-- Cpu -->
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.IconSource>
<controls:FASymbolIconSource
FontSize="10"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Symbol="fa-solid fa-microchip" />
</ui:SettingsExpanderItem.IconSource>
<sg:SpacedGrid
DataContext="{Binding CpuInfoAsync^}"
ColumnDefinitions="Auto,Auto"
ColumnSpacing="16"
RowDefinitions="Auto,Auto">
<TextBlock Grid.Column="0" Text="CPU" />
<SelectableTextBlock
Grid.Row="0"
Grid.Column="1"
Foreground="{DynamicResource TextControlPlaceholderForeground}"
Text="{Binding ProcessorCaption}"
TextWrapping="WrapWithOverflow" />
</sg:SpacedGrid>
</ui:SettingsExpanderItem>
<!-- Memory -->
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.IconSource>
<controls:FASymbolIconSource
FontSize="10"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Symbol="fa-solid fa-memory" />
</ui:SettingsExpanderItem.IconSource>
<sg:SpacedGrid
DataContext="{Binding MemoryInfo}"
ColumnDefinitions="Auto,Auto"
ColumnSpacing="16"
RowDefinitions="Auto,Auto">
<TextBlock Grid.Column="0" Text="Total Memory" />
<SelectableTextBlock
Grid.Row="0"
Grid.Column="1"
Foreground="{DynamicResource TextControlPlaceholderForeground}"
TextWrapping="WrapWithOverflow">
<SelectableTextBlock.Text>
<MultiBinding StringFormat="{}{0} ({1} usable)">
<Binding Path="TotalInstalledBytes" Converter="{x:Static converters:StringFormatConverters.MemoryBytes}"/>
<Binding Path="TotalPhysicalBytes" Converter="{x:Static converters:StringFormatConverters.MemoryBytes}"/>
</MultiBinding>
</SelectableTextBlock.Text>
</SelectableTextBlock>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Text="Available Memory" />
<SelectableTextBlock
Grid.Row="1"
Grid.Column="1"
Foreground="{DynamicResource TextControlPlaceholderForeground}"
Text="{Binding AvailablePhysicalBytes, Converter={x:Static converters:StringFormatConverters.MemoryBytes}}"
TextWrapping="WrapWithOverflow" />
</sg:SpacedGrid>
</ui:SettingsExpanderItem>
<!-- GPUs -->
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.IconSource>
<controls:FASymbolIconSource
FontSize="10"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Symbol="fa-solid fa-tachograph-digital" />
</ui:SettingsExpanderItem.IconSource>
<ItemsControl ItemsSource="{Binding GpuInfos}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="hardwareInfo:GpuInfo">
<sg:SpacedGrid ColumnDefinitions="Auto,Auto,Auto" RowDefinitions="Auto,Auto">
<TextBlock Text="{Binding Index, StringFormat={}{0}, Converter={StaticResource IndexPlusOneConverter}}" Theme="{DynamicResource BodyStrongTextBlockStyle}" />
<SelectableTextBlock
Grid.Row="0"
Grid.Column="1"
Text="{Binding Name}" />
<SelectableTextBlock
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Foreground="{DynamicResource TextControlPlaceholderForeground}"
IsVisible="{Binding !!MemoryBytes}"
Text="{Binding MemoryBytes, Converter={x:Static converters:StringFormatConverters.MemoryBytes}}"
TextWrapping="WrapWithOverflow" />
</sg:SpacedGrid>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</sg:SpacedGrid> </sg:SpacedGrid>
<!-- Debug Options --> <!-- Debug Options -->

3
StabilityMatrix.Avalonia/Views/SettingsPage.axaml

@ -19,10 +19,13 @@
mc:Ignorable="d"> mc:Ignorable="d">
<controls:UserControlBase.Resources> <controls:UserControlBase.Resources>
<!-- Override styles for BreadcrumbBar -->
<!-- ReSharper disable Xaml.RedundantResource -->
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">24</x:Double> <x:Double x:Key="BreadcrumbBarItemThemeFontSize">24</x:Double>
<x:Double x:Key="BreadcrumbBarChevronFontSize">17</x:Double> <x:Double x:Key="BreadcrumbBarChevronFontSize">17</x:Double>
<Thickness x:Key="BreadcrumbBarChevronPadding">6,3</Thickness> <Thickness x:Key="BreadcrumbBarChevronPadding">6,3</Thickness>
<FontWeight x:Key="BreadcrumbBarItemFontWeight">Medium</FontWeight> <FontWeight x:Key="BreadcrumbBarItemFontWeight">Medium</FontWeight>
<!-- ReSharper restore Xaml.RedundantResource -->
</controls:UserControlBase.Resources> </controls:UserControlBase.Resources>
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto,*">

46
StabilityMatrix.Core/Database/LiteDbContext.cs

@ -23,8 +23,7 @@ public class LiteDbContext : ILiteDbContext
public event EventHandler? CivitModelsChanged; public event EventHandler? CivitModelsChanged;
// Collections (Tables) // Collections (Tables)
public ILiteCollectionAsync<CivitModel> CivitModels => public ILiteCollectionAsync<CivitModel> CivitModels => Database.GetCollection<CivitModel>("CivitModels");
Database.GetCollection<CivitModel>("CivitModels");
public ILiteCollectionAsync<CivitModelVersion> CivitModelVersions => public ILiteCollectionAsync<CivitModelVersion> CivitModelVersions =>
Database.GetCollection<CivitModelVersion>("CivitModelVersions"); Database.GetCollection<CivitModelVersion>("CivitModelVersions");
public ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache => public ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache =>
@ -64,19 +63,12 @@ public class LiteDbContext : ILiteDbContext
{ {
var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db"); var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db");
db = new LiteDatabaseAsync( db = new LiteDatabaseAsync(
new ConnectionString() new ConnectionString() { Filename = dbPath, Connection = ConnectionType.Shared, }
{
Filename = dbPath,
Connection = ConnectionType.Shared,
}
); );
} }
catch (IOException e) catch (IOException e)
{ {
logger.LogWarning( logger.LogWarning("Database in use or not accessible ({Message}), using temporary database", e.Message);
"Database in use or not accessible ({Message}), using temporary database",
e.Message
);
} }
} }
@ -84,30 +76,18 @@ public class LiteDbContext : ILiteDbContext
db ??= new LiteDatabaseAsync(":temp:"); db ??= new LiteDatabaseAsync(":temp:");
// Register reference fields // Register reference fields
LiteDBExtensions.Register<CivitModel, CivitModelVersion>( LiteDBExtensions.Register<CivitModel, CivitModelVersion>(m => m.ModelVersions, "CivitModelVersions");
m => m.ModelVersions, LiteDBExtensions.Register<CivitModelQueryCacheEntry, CivitModel>(e => e.Items, "CivitModels");
"CivitModelVersions"
);
LiteDBExtensions.Register<CivitModelQueryCacheEntry, CivitModel>(
e => e.Items,
"CivitModels"
);
return db; return db;
} }
public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync( public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3)
string hashBlake3
)
{ {
var version = await CivitModelVersions var version = await CivitModelVersions
.Query() .Query()
.Where( .Where(
mv => mv => mv.Files!.Select(f => f.Hashes).Select(hashes => hashes.BLAKE3).Any(hash => hash == hashBlake3)
mv.Files!
.Select(f => f.Hashes)
.Select(hashes => hashes.BLAKE3)
.Any(hash => hash == hashBlake3)
) )
.FirstOrDefaultAsync() .FirstOrDefaultAsync()
.ConfigureAwait(false); .ConfigureAwait(false);
@ -128,9 +108,7 @@ public class LiteDbContext : ILiteDbContext
public async Task<bool> UpsertCivitModelAsync(CivitModel civitModel) public async Task<bool> UpsertCivitModelAsync(CivitModel civitModel)
{ {
// Insert model versions first then model // Insert model versions first then model
var versionsUpdated = await CivitModelVersions var versionsUpdated = await CivitModelVersions.UpsertAsync(civitModel.ModelVersions).ConfigureAwait(false);
.UpsertAsync(civitModel.ModelVersions)
.ConfigureAwait(false);
var updated = await CivitModels.UpsertAsync(civitModel).ConfigureAwait(false); var updated = await CivitModels.UpsertAsync(civitModel).ConfigureAwait(false);
// Notify listeners on any change // Notify listeners on any change
var anyUpdated = versionsUpdated > 0 || updated; var anyUpdated = versionsUpdated > 0 || updated;
@ -182,8 +160,7 @@ public class LiteDbContext : ILiteDbContext
return null; return null;
} }
public Task<bool> UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) => public Task<bool> UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) => GithubCache.UpsertAsync(cacheEntry);
GithubCache.UpsertAsync(cacheEntry);
public void Dispose() public void Dispose()
{ {
@ -194,6 +171,11 @@ public class LiteDbContext : ILiteDbContext
database.Dispose(); database.Dispose();
} }
catch (ObjectDisposedException) { } catch (ObjectDisposedException) { }
catch (ApplicationException)
{
// Ignores a mutex error from library
// https://stability-matrix.sentry.io/share/issue/5c62f37462444e7eab18cea314af231f/
}
database = null; database = null;
} }

172
StabilityMatrix.Core/Helper/HardwareHelper.cs

@ -1,172 +0,0 @@
using System.Diagnostics;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using Microsoft.Win32;
namespace StabilityMatrix.Core.Helper;
public static partial class HardwareHelper
{
private static IReadOnlyList<GpuInfo>? cachedGpuInfos;
private static string RunBashCommand(string command)
{
var processInfo = new ProcessStartInfo("bash", "-c \"" + command + "\"")
{
UseShellExecute = false,
RedirectStandardOutput = true
};
var process = Process.Start(processInfo);
process.WaitForExit();
var output = process.StandardOutput.ReadToEnd();
return output;
}
[SupportedOSPlatform("windows")]
private static IEnumerable<GpuInfo> IterGpuInfoWindows()
{
const string gpuRegistryKeyPath =
@"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}";
using var baseKey = Registry.LocalMachine.OpenSubKey(gpuRegistryKeyPath);
if (baseKey == null) yield break;
foreach (var subKeyName in baseKey.GetSubKeyNames().Where(k => k.StartsWith("0")))
{
using var subKey = baseKey.OpenSubKey(subKeyName);
if (subKey != null)
{
yield return new GpuInfo
{
Name = subKey.GetValue("DriverDesc")?.ToString(),
MemoryBytes = Convert.ToUInt64(subKey.GetValue("HardwareInformation.qwMemorySize")),
};
}
}
}
[SupportedOSPlatform("linux")]
private static IEnumerable<GpuInfo> IterGpuInfoLinux()
{
var output = RunBashCommand("lspci | grep VGA");
var gpuLines = output.Split("\n");
foreach (var line in gpuLines)
{
if (string.IsNullOrWhiteSpace(line)) continue;
var gpuId = line.Split(' ')[0]; // The GPU ID is the first part of the line
var gpuOutput = RunBashCommand($"lspci -v -s {gpuId}");
ulong memoryBytes = 0;
string? name = null;
// Parse output with regex
var match = Regex.Match(gpuOutput, @"VGA compatible controller: ([^\n]*)");
if (match.Success)
{
name = match.Groups[1].Value.Trim();
}
match = Regex.Match(gpuOutput, @"prefetchable\) \[size=(\\d+)M\]");
if (match.Success)
{
memoryBytes = ulong.Parse(match.Groups[1].Value) * 1024 * 1024;
}
yield return new GpuInfo { Name = name, MemoryBytes = memoryBytes };
}
}
/// <summary>
/// Yields GpuInfo for each GPU in the system.
/// </summary>
public static IEnumerable<GpuInfo> IterGpuInfo()
{
if (Compat.IsWindows)
{
return IterGpuInfoWindows();
}
else if (Compat.IsLinux)
{
// Since this requires shell commands, fetch cached value if available.
if (cachedGpuInfos is not null)
{
return cachedGpuInfos;
}
// No cache, fetch and cache.
cachedGpuInfos = IterGpuInfoLinux().ToList();
return cachedGpuInfos;
}
// TODO: Implement for macOS
return Enumerable.Empty<GpuInfo>();
}
/// <summary>
/// Return true if the system has at least one Nvidia GPU.
/// </summary>
public static bool HasNvidiaGpu()
{
return IterGpuInfo().Any(gpu => gpu.IsNvidia);
}
/// <summary>
/// Return true if the system has at least one AMD GPU.
/// </summary>
public static bool HasAmdGpu()
{
return IterGpuInfo().Any(gpu => gpu.IsAmd);
}
// Set ROCm for default if AMD and Linux
public static bool PreferRocm() => !HasNvidiaGpu()
&& HasAmdGpu()
&& Compat.IsLinux;
// Set DirectML for default if AMD and Windows
public static bool PreferDirectML() => !HasNvidiaGpu()
&& HasAmdGpu()
&& Compat.IsWindows;
}
public enum Level
{
Unknown,
Low,
Medium,
High
}
public record GpuInfo
{
public string? Name { get; init; } = string.Empty;
public ulong MemoryBytes { get; init; }
public Level? MemoryLevel => MemoryBytes switch
{
<= 0 => Level.Unknown,
< 4 * Size.GiB => Level.Low,
< 8 * Size.GiB => Level.Medium,
_ => Level.High
};
public bool IsNvidia
{
get
{
var name = Name?.ToLowerInvariant();
if (string.IsNullOrEmpty(name)) return false;
return name.Contains("nvidia")
|| name.Contains("tesla");
}
}
public bool IsAmd => Name?.ToLowerInvariant().Contains("amd") ?? false;
}

7
StabilityMatrix.Core/Helper/HardwareInfo/CpuInfo.cs

@ -0,0 +1,7 @@
namespace StabilityMatrix.Core.Helper.HardwareInfo;
public readonly record struct CpuInfo
{
public string ProcessorCaption { get; init; }
public string ProcessorName { get; init; }
}

31
StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs

@ -0,0 +1,31 @@
namespace StabilityMatrix.Core.Helper.HardwareInfo;
public record GpuInfo
{
public int Index { get; init; }
public string? Name { get; init; } = string.Empty;
public ulong MemoryBytes { get; init; }
public MemoryLevel? MemoryLevel =>
MemoryBytes switch
{
<= 0 => HardwareInfo.MemoryLevel.Unknown,
< 4 * Size.GiB => HardwareInfo.MemoryLevel.Low,
< 8 * Size.GiB => HardwareInfo.MemoryLevel.Medium,
_ => HardwareInfo.MemoryLevel.High
};
public bool IsNvidia
{
get
{
var name = Name?.ToLowerInvariant();
if (string.IsNullOrEmpty(name))
return false;
return name.Contains("nvidia") || name.Contains("tesla");
}
}
public bool IsAmd => Name?.Contains("amd", StringComparison.OrdinalIgnoreCase) ?? false;
}

234
StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs

@ -0,0 +1,234 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using Hardware.Info;
using Microsoft.Win32;
namespace StabilityMatrix.Core.Helper.HardwareInfo;
public static partial class HardwareHelper
{
private static IReadOnlyList<GpuInfo>? cachedGpuInfos;
private static readonly Lazy<IHardwareInfo> HardwareInfoLazy = new(() => new Hardware.Info.HardwareInfo());
public static IHardwareInfo HardwareInfo => HardwareInfoLazy.Value;
private static string RunBashCommand(string command)
{
var processInfo = new ProcessStartInfo("bash", "-c \"" + command + "\"")
{
UseShellExecute = false,
RedirectStandardOutput = true
};
var process = Process.Start(processInfo);
process.WaitForExit();
var output = process.StandardOutput.ReadToEnd();
return output;
}
[SupportedOSPlatform("windows")]
private static IEnumerable<GpuInfo> IterGpuInfoWindows()
{
const string gpuRegistryKeyPath =
@"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}";
using var baseKey = Registry.LocalMachine.OpenSubKey(gpuRegistryKeyPath);
if (baseKey == null)
yield break;
var gpuIndex = 0;
foreach (var subKeyName in baseKey.GetSubKeyNames().Where(k => k.StartsWith("0")))
{
using var subKey = baseKey.OpenSubKey(subKeyName);
if (subKey != null)
{
yield return new GpuInfo
{
Index = gpuIndex++,
Name = subKey.GetValue("DriverDesc")?.ToString(),
MemoryBytes = Convert.ToUInt64(subKey.GetValue("HardwareInformation.qwMemorySize")),
};
}
}
}
[SupportedOSPlatform("linux")]
private static IEnumerable<GpuInfo> IterGpuInfoLinux()
{
var output = RunBashCommand("lspci | grep VGA");
var gpuLines = output.Split("\n");
var gpuIndex = 0;
foreach (var line in gpuLines)
{
if (string.IsNullOrWhiteSpace(line))
continue;
var gpuId = line.Split(' ')[0]; // The GPU ID is the first part of the line
var gpuOutput = RunBashCommand($"lspci -v -s {gpuId}");
ulong memoryBytes = 0;
string? name = null;
// Parse output with regex
var match = Regex.Match(gpuOutput, @"VGA compatible controller: ([^\n]*)");
if (match.Success)
{
name = match.Groups[1].Value.Trim();
}
match = Regex.Match(gpuOutput, @"prefetchable\) \[size=(\\d+)M\]");
if (match.Success)
{
memoryBytes = ulong.Parse(match.Groups[1].Value) * 1024 * 1024;
}
yield return new GpuInfo
{
Index = gpuIndex++,
Name = name,
MemoryBytes = memoryBytes
};
}
}
/// <summary>
/// Yields GpuInfo for each GPU in the system.
/// </summary>
public static IEnumerable<GpuInfo> IterGpuInfo()
{
if (Compat.IsWindows)
{
return IterGpuInfoWindows();
}
else if (Compat.IsLinux)
{
// Since this requires shell commands, fetch cached value if available.
if (cachedGpuInfos is not null)
{
return cachedGpuInfos;
}
// No cache, fetch and cache.
cachedGpuInfos = IterGpuInfoLinux().ToList();
return cachedGpuInfos;
}
// TODO: Implement for macOS
return Enumerable.Empty<GpuInfo>();
}
/// <summary>
/// Return true if the system has at least one Nvidia GPU.
/// </summary>
public static bool HasNvidiaGpu()
{
return IterGpuInfo().Any(gpu => gpu.IsNvidia);
}
/// <summary>
/// Return true if the system has at least one AMD GPU.
/// </summary>
public static bool HasAmdGpu()
{
return IterGpuInfo().Any(gpu => gpu.IsAmd);
}
// Set ROCm for default if AMD and Linux
public static bool PreferRocm() => !HasNvidiaGpu() && HasAmdGpu() && Compat.IsLinux;
// Set DirectML for default if AMD and Windows
public static bool PreferDirectML() => !HasNvidiaGpu() && HasAmdGpu() && Compat.IsWindows;
/// <summary>
/// Gets the total and available physical memory in bytes.
/// </summary>
public static MemoryInfo GetMemoryInfo() =>
Compat.IsWindows ? GetMemoryInfoImplWindows() : GetMemoryInfoImplGeneric();
[SupportedOSPlatform("windows")]
private static MemoryInfo GetMemoryInfoImplWindows()
{
var memoryStatus = new Win32MemoryStatusEx();
if (!GlobalMemoryStatusEx(ref memoryStatus))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (!GetPhysicallyInstalledSystemMemory(out var installedMemoryKb))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
return new MemoryInfo
{
TotalInstalledBytes = (ulong)installedMemoryKb * 1024,
TotalPhysicalBytes = memoryStatus.UllTotalPhys,
AvailablePhysicalBytes = memoryStatus.UllAvailPhys
};
}
private static MemoryInfo GetMemoryInfoImplGeneric()
{
HardwareInfo.RefreshMemoryList();
return new MemoryInfo
{
TotalPhysicalBytes = HardwareInfo.MemoryStatus.TotalPhysical,
AvailablePhysicalBytes = HardwareInfo.MemoryStatus.AvailablePhysical
};
}
/// <summary>
/// Gets cpu info
/// </summary>
public static Task<CpuInfo> GetCpuInfoAsync() =>
Compat.IsWindows ? Task.FromResult(GetCpuInfoImplWindows()) : GetCpuInfoImplGenericAsync();
[SupportedOSPlatform("windows")]
private static CpuInfo GetCpuInfoImplWindows()
{
var info = new CpuInfo();
using var processorKey = Registry
.LocalMachine
.OpenSubKey(@"Hardware\Description\System\CentralProcessor\0", RegistryKeyPermissionCheck.ReadSubTree);
if (processorKey?.GetValue("ProcessorNameString") is string processorName)
{
info = info with { ProcessorCaption = processorName.Trim() };
}
return info;
}
private static Task<CpuInfo> GetCpuInfoImplGenericAsync()
{
return Task.Run(() =>
{
HardwareInfo.RefreshCPUList();
return new CpuInfo { ProcessorCaption = HardwareInfo.CpuList.FirstOrDefault()?.Caption.Trim() ?? "" };
});
}
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetPhysicallyInstalledSystemMemory(out long totalMemoryInKilobytes);
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GlobalMemoryStatusEx(ref Win32MemoryStatusEx lpBuffer);
}

10
StabilityMatrix.Core/Helper/HardwareInfo/MemoryInfo.cs

@ -0,0 +1,10 @@
namespace StabilityMatrix.Core.Helper.HardwareInfo;
public readonly record struct MemoryInfo
{
public ulong TotalInstalledBytes { get; init; }
public ulong TotalPhysicalBytes { get; init; }
public ulong AvailablePhysicalBytes { get; init; }
}

9
StabilityMatrix.Core/Helper/HardwareInfo/MemoryLevel.cs

@ -0,0 +1,9 @@
namespace StabilityMatrix.Core.Helper.HardwareInfo;
public enum MemoryLevel
{
Unknown,
Low,
Medium,
High
}

19
StabilityMatrix.Core/Helper/HardwareInfo/Win32MemoryStatusEx.cs

@ -0,0 +1,19 @@
using System.Runtime.InteropServices;
namespace StabilityMatrix.Core.Helper.HardwareInfo;
[StructLayout(LayoutKind.Sequential)]
public struct Win32MemoryStatusEx
{
public uint DwLength = (uint)Marshal.SizeOf(typeof(Win32MemoryStatusEx));
public uint DwMemoryLoad = 0;
public ulong UllTotalPhys = 0;
public ulong UllAvailPhys = 0;
public ulong UllTotalPageFile = 0;
public ulong UllAvailPageFile = 0;
public ulong UllTotalVirtual = 0;
public ulong UllAvailVirtual = 0;
public ulong UllAvailExtendedVirtual = 0;
public Win32MemoryStatusEx() { }
}

37
StabilityMatrix.Core/Helper/Size.cs

@ -9,25 +9,42 @@ public static class Size
public const ulong MiB = KiB * 1024; public const ulong MiB = KiB * 1024;
public const ulong GiB = MiB * 1024; public const ulong GiB = MiB * 1024;
public static string FormatBytes(ulong bytes) private static string TrimZero(string value)
{
return value.TrimEnd('0').TrimEnd('.');
}
public static string FormatBytes(ulong bytes, bool trimZero = false)
{ {
return bytes switch return bytes switch
{ {
< KiB => $"{bytes} B", < KiB => $"{bytes:0} Bytes",
< MiB => $"{bytes / (double)KiB:0.0} KiB", < MiB
< GiB => $"{bytes / (double)MiB:0.0} MiB", => (trimZero ? $"{bytes / (double)KiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)KiB:0.0}")
_ => $"{bytes / (double)GiB:0.0} GiB" + " KiB",
< GiB
=> (trimZero ? $"{bytes / (double)MiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)MiB:0.0}")
+ " MiB",
_
=> (trimZero ? $"{bytes / (double)GiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)GiB:0.0}")
+ " GiB"
}; };
} }
public static string FormatBase10Bytes(ulong bytes) public static string FormatBase10Bytes(ulong bytes, bool trimZero = false)
{ {
return bytes switch return bytes switch
{ {
< KiB => $"{bytes} Bytes", < KiB => $"{bytes:0} Bytes",
< MiB => $"{bytes / (double)KiB:0.0} KB", < MiB
< GiB => $"{bytes / (double)MiB:0.0} MB", => (trimZero ? $"{bytes / (double)KiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)KiB:0.0}")
_ => $"{bytes / (double)GiB:0.00} GB" + " KB",
< GiB
=> (trimZero ? $"{bytes / (double)MiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)MiB:0.0}")
+ " MB",
_
=> (trimZero ? $"{bytes / (double)GiB:0.00}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)GiB:0.00}")
+ " GB"
}; };
} }

8
StabilityMatrix.Core/Models/GlobalConfig.cs

@ -19,8 +19,7 @@ public static class GlobalConfig
{ {
if (libraryDir is null) if (libraryDir is null)
{ {
throw new NullReferenceException( throw new NullReferenceException("GlobalConfig.LibraryDir was not set before being accessed.");
"GlobalConfig.LibraryDir was not set before being accessed.");
} }
return libraryDir; return libraryDir;
} }
@ -31,11 +30,12 @@ public static class GlobalConfig
/// Full path to the %APPDATA% directory. /// Full path to the %APPDATA% directory.
/// Usually C:\Users\{username}\AppData\Roaming /// Usually C:\Users\{username}\AppData\Roaming
/// </summary> /// </summary>
public static DirectoryPath AppDataDir { get; } = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); public static DirectoryPath AppDataDir { get; } =
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
/// <summary> /// <summary>
/// Full path to the fixed home directory. /// Full path to the fixed home directory.
/// Currently %APPDATA%\StabilityMatrix /// Currently %APPDATA%\StabilityMatrix
///</summary> ///</summary>
public static DirectoryPath HomeDir { get; } = AppDataDir.JoinDir("StabilityMatrix"); public static DirectoryPath HomeDir { get; set; } = AppDataDir.JoinDir("StabilityMatrix");
} }

20
StabilityMatrix.Core/Models/InstalledPackage.cs

@ -48,8 +48,13 @@ public class InstalledPackage : IJsonOnDeserialized
public List<LaunchOption>? LaunchArgs { get; set; } public List<LaunchOption>? LaunchArgs { get; set; }
public DateTimeOffset? LastUpdateCheck { get; set; } public DateTimeOffset? LastUpdateCheck { get; set; }
public bool UpdateAvailable { get; set; } public bool UpdateAvailable { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter<TorchVersion>))]
public TorchVersion? PreferredTorchVersion { get; set; } public TorchVersion? PreferredTorchVersion { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter<SharedFolderMethod>))]
public SharedFolderMethod? PreferredSharedFolderMethod { get; set; } public SharedFolderMethod? PreferredSharedFolderMethod { get; set; }
public bool UseSharedOutputFolder { get; set; } public bool UseSharedOutputFolder { get; set; }
/// <summary> /// <summary>
@ -188,10 +193,7 @@ public class InstalledPackage : IJsonOnDeserialized
var suffix = 2; var suffix = 2;
while (Directory.Exists(newPackagePath)) while (Directory.Exists(newPackagePath))
{ {
newPackagePath = System.IO.Path.Combine( newPackagePath = System.IO.Path.Combine(newPackagesDir, $"{packageFolderName}-{suffix}");
newPackagesDir,
$"{packageFolderName}-{suffix}"
);
suffix++; suffix++;
} }
@ -234,16 +236,10 @@ public class InstalledPackage : IJsonOnDeserialized
if (Version != null) if (Version != null)
return; return;
if ( if (string.IsNullOrWhiteSpace(InstalledBranch) && !string.IsNullOrWhiteSpace(PackageVersion))
string.IsNullOrWhiteSpace(InstalledBranch) && !string.IsNullOrWhiteSpace(PackageVersion)
)
{ {
// release mode // release mode
Version = new InstalledPackageVersion Version = new InstalledPackageVersion { InstalledReleaseVersion = PackageVersion, IsPrerelease = false };
{
InstalledReleaseVersion = PackageVersion,
IsPrerelease = false
};
} }
else if (!string.IsNullOrWhiteSpace(PackageVersion)) else if (!string.IsNullOrWhiteSpace(PackageVersion))
{ {

7
StabilityMatrix.Core/Models/LaunchOptionType.cs

@ -1,8 +1,11 @@
namespace StabilityMatrix.Core.Models; using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models;
[JsonConverter(typeof(JsonStringEnumConverter<LaunchOptionType>))]
public enum LaunchOptionType public enum LaunchOptionType
{ {
Bool, Bool,
String, String,
Int, Int
} }

26
StabilityMatrix.Core/Models/Packages/A3WebUI.cs

@ -5,6 +5,7 @@ using NLog;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
@ -24,8 +25,7 @@ public class A3WebUI : BaseGitPackage
public override string LicenseType => "AGPL-3.0"; public override string LicenseType => "AGPL-3.0";
public override string LicenseUrl => public override string LicenseUrl =>
"https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/master/LICENSE.txt"; "https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/master/LICENSE.txt";
public override string Blurb => public override string Blurb => "A browser interface based on Gradio library for Stable Diffusion";
"A browser interface based on Gradio library for Stable Diffusion";
public override string LaunchCommand => "launch.py"; public override string LaunchCommand => "launch.py";
public override Uri PreviewImageUri => public override Uri PreviewImageUri =>
new("https://github.com/AUTOMATIC1111/stable-diffusion-webui/raw/master/screenshot.png"); new("https://github.com/AUTOMATIC1111/stable-diffusion-webui/raw/master/screenshot.png");
@ -98,13 +98,10 @@ public class A3WebUI : BaseGitPackage
{ {
Name = "VRAM", Name = "VRAM",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
{ {
Level.Low => "--lowvram", MemoryLevel.Low => "--lowvram",
Level.Medium => "--medvram", MemoryLevel.Medium => "--medvram",
_ => null _ => null
}, },
Options = new() { "--lowvram", "--medvram", "--medvram-sdxl" } Options = new() { "--lowvram", "--medvram", "--medvram-sdxl" }
@ -214,9 +211,7 @@ public class A3WebUI : BaseGitPackage
} }
// Install requirements file // Install requirements file
progress?.Report( progress?.Report(new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true));
new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true)
);
Logger.Info("Installing requirements_versions.txt"); Logger.Info("Installing requirements_versions.txt");
var requirements = new FilePath(installLocation, "requirements_versions.txt"); var requirements = new FilePath(installLocation, "requirements_versions.txt");
@ -274,18 +269,13 @@ public class A3WebUI : BaseGitPackage
Action<ProcessOutput>? onConsoleOutput = null Action<ProcessOutput>? onConsoleOutput = null
) )
{ {
progress?.Report( progress?.Report(new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true));
new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true)
);
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false);
await venvRunner await venvRunner
.PipInstall( .PipInstall(
new PipInstallArgs() new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision().WithTorchExtraIndex("rocm5.1.1"),
.WithTorch("==2.0.1")
.WithTorchVision()
.WithTorchExtraIndex("rocm5.1.1"),
onConsoleOutput onConsoleOutput
) )
.ConfigureAwait(false); .ConfigureAwait(false);

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

@ -1,5 +1,6 @@
using Octokit; using Octokit;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
@ -80,29 +81,15 @@ public abstract class BasePackage
); );
public virtual IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods => public virtual IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] new[] { SharedFolderMethod.Symlink, SharedFolderMethod.Configuration, SharedFolderMethod.None };
{
SharedFolderMethod.Symlink,
SharedFolderMethod.Configuration,
SharedFolderMethod.None
};
public abstract SharedFolderMethod RecommendedSharedFolderMethod { get; } public abstract SharedFolderMethod RecommendedSharedFolderMethod { get; }
public abstract Task SetupModelFolders( public abstract Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod);
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
);
public abstract Task UpdateModelFolders( public abstract Task UpdateModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod);
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
);
public abstract Task RemoveModelFolderLinks( public abstract Task RemoveModelFolderLinks(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod);
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
);
public abstract Task SetupOutputFolderLinks(DirectoryPath installDirectory); public abstract Task SetupOutputFolderLinks(DirectoryPath installDirectory);
public abstract Task RemoveOutputFolderLinks(DirectoryPath installDirectory); public abstract Task RemoveOutputFolderLinks(DirectoryPath installDirectory);
@ -125,10 +112,7 @@ public abstract class BasePackage
return TorchVersion.Rocm; return TorchVersion.Rocm;
} }
if ( if (HardwareHelper.PreferDirectML() && AvailableTorchVersions.Contains(TorchVersion.DirectMl))
HardwareHelper.PreferDirectML()
&& AvailableTorchVersions.Contains(TorchVersion.DirectMl)
)
{ {
return TorchVersion.DirectMl; return TorchVersion.DirectMl;
} }
@ -155,20 +139,11 @@ public abstract class BasePackage
/// Mapping of <see cref="SharedFolderType"/> to the relative paths from the package root. /// Mapping of <see cref="SharedFolderType"/> to the relative paths from the package root.
/// </summary> /// </summary>
public abstract Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; } public abstract Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public abstract Dictionary< public abstract Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders { get; }
SharedOutputType,
IReadOnlyList<string>
>? SharedOutputFolders { get; }
public abstract Task<PackageVersionOptions> GetAllVersionOptions(); public abstract Task<PackageVersionOptions> GetAllVersionOptions();
public abstract Task<IEnumerable<GitCommit>?> GetAllCommits( public abstract Task<IEnumerable<GitCommit>?> GetAllCommits(string branch, int page = 1, int perPage = 10);
string branch, public abstract Task<DownloadPackageVersionOptions> GetLatestVersion(bool includePrerelease = false);
int page = 1,
int perPage = 10
);
public abstract Task<DownloadPackageVersionOptions> GetLatestVersion(
bool includePrerelease = false
);
public abstract string MainBranch { get; } public abstract string MainBranch { get; }
public event EventHandler<int>? Exited; public event EventHandler<int>? Exited;
public event EventHandler<string>? StartupComplete; public event EventHandler<string>? StartupComplete;
@ -178,9 +153,7 @@ public abstract class BasePackage
public void OnStartupComplete(string url) => StartupComplete?.Invoke(this, url); public void OnStartupComplete(string url) => StartupComplete?.Invoke(this, url);
public virtual PackageVersionType AvailableVersionTypes => public virtual PackageVersionType AvailableVersionTypes =>
ShouldIgnoreReleases ShouldIgnoreReleases ? PackageVersionType.Commit : PackageVersionType.GithubRelease | PackageVersionType.Commit;
? PackageVersionType.Commit
: PackageVersionType.GithubRelease | PackageVersionType.Commit;
protected async Task InstallCudaTorch( protected async Task InstallCudaTorch(
PyVenvRunner venvRunner, PyVenvRunner venvRunner,
@ -188,9 +161,7 @@ public abstract class BasePackage
Action<ProcessOutput>? onConsoleOutput = null Action<ProcessOutput>? onConsoleOutput = null
) )
{ {
progress?.Report( progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true));
new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true)
);
await venvRunner await venvRunner
.PipInstall( .PipInstall(
@ -210,9 +181,7 @@ public abstract class BasePackage
Action<ProcessOutput>? onConsoleOutput = null Action<ProcessOutput>? onConsoleOutput = null
) )
{ {
progress?.Report( progress?.Report(new ProgressReport(-1f, "Installing PyTorch for DirectML", isIndeterminate: true));
new ProgressReport(-1f, "Installing PyTorch for DirectML", isIndeterminate: true)
);
return venvRunner.PipInstall(new PipInstallArgs().WithTorchDirectML(), onConsoleOutput); return venvRunner.PipInstall(new PipInstallArgs().WithTorchDirectML(), onConsoleOutput);
} }
@ -223,13 +192,8 @@ public abstract class BasePackage
Action<ProcessOutput>? onConsoleOutput = null Action<ProcessOutput>? onConsoleOutput = null
) )
{ {
progress?.Report( progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CPU", isIndeterminate: true));
new ProgressReport(-1f, "Installing PyTorch for CPU", isIndeterminate: true)
);
return venvRunner.PipInstall( return venvRunner.PipInstall(new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision(), onConsoleOutput);
new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision(),
onConsoleOutput
);
} }
} }

99
StabilityMatrix.Core/Models/Packages/ComfyUI.cs

@ -4,6 +4,7 @@ using NLog;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
@ -23,8 +24,7 @@ public class ComfyUI : BaseGitPackage
public override string DisplayName { get; set; } = "ComfyUI"; public override string DisplayName { get; set; } = "ComfyUI";
public override string Author => "comfyanonymous"; public override string Author => "comfyanonymous";
public override string LicenseType => "GPL-3.0"; public override string LicenseType => "GPL-3.0";
public override string LicenseUrl => public override string LicenseUrl => "https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE";
"https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE";
public override string Blurb => "A powerful and modular stable diffusion GUI and backend"; public override string Blurb => "A powerful and modular stable diffusion GUI and backend";
public override string LaunchCommand => "main.py"; public override string LaunchCommand => "main.py";
@ -35,8 +35,7 @@ public class ComfyUI : BaseGitPackage
public override string OutputFolderName => "output"; public override string OutputFolderName => "output";
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced;
public override SharedFolderMethod RecommendedSharedFolderMethod => public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration;
SharedFolderMethod.Configuration;
public ComfyUI( public ComfyUI(
IGithubApiCache githubApi, IGithubApiCache githubApi,
@ -87,13 +86,10 @@ public class ComfyUI : BaseGitPackage
{ {
Name = "VRAM", Name = "VRAM",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
{ {
Level.Low => "--lowvram", MemoryLevel.Low => "--lowvram",
Level.Medium => "--normalvram", MemoryLevel.Medium => "--normalvram",
_ => null _ => null
}, },
Options = { "--highvram", "--normalvram", "--lowvram", "--novram" } Options = { "--highvram", "--normalvram", "--lowvram", "--novram" }
@ -103,12 +99,7 @@ public class ComfyUI : BaseGitPackage
Name = "Preview Method", Name = "Preview Method",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
InitialValue = "--preview-method auto", InitialValue = "--preview-method auto",
Options = Options = { "--preview-method auto", "--preview-method latent2rgb", "--preview-method taesd" }
{
"--preview-method auto",
"--preview-method latent2rgb",
"--preview-method taesd"
}
}, },
new() new()
{ {
@ -149,14 +140,7 @@ public class ComfyUI : BaseGitPackage
public override string MainBranch => "master"; public override string MainBranch => "master";
public override IEnumerable<TorchVersion> AvailableTorchVersions => public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm, TorchVersion.Mps };
{
TorchVersion.Cpu,
TorchVersion.Cuda,
TorchVersion.DirectMl,
TorchVersion.Rocm,
TorchVersion.Mps
};
public override async Task InstallPackage( public override async Task InstallPackage(
string installLocation, string installLocation,
@ -217,9 +201,7 @@ public class ComfyUI : BaseGitPackage
} }
// Install requirements file (skip torch) // Install requirements file (skip torch)
progress?.Report( progress?.Report(new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true));
new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true)
);
var requirementsFile = new FilePath(installLocation, "requirements.txt"); var requirementsFile = new FilePath(installLocation, "requirements.txt");
@ -227,15 +209,10 @@ public class ComfyUI : BaseGitPackage
.PipInstallFromRequirements(requirementsFile, onConsoleOutput, excludes: "torch") .PipInstallFromRequirements(requirementsFile, onConsoleOutput, excludes: "torch")
.ConfigureAwait(false); .ConfigureAwait(false);
progress?.Report( progress?.Report(new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false));
new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false)
);
} }
private async Task AutoDetectAndInstallTorch( private async Task AutoDetectAndInstallTorch(PyVenvRunner venvRunner, IProgress<ProgressReport>? progress = null)
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null
)
{ {
var gpus = HardwareHelper.IterGpuInfo().ToList(); var gpus = HardwareHelper.IterGpuInfo().ToList();
if (gpus.Any(g => g.IsNvidia)) if (gpus.Any(g => g.IsNvidia))
@ -292,10 +269,7 @@ public class ComfyUI : BaseGitPackage
VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit); VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
} }
public override Task SetupModelFolders( public override Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod)
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
switch (sharedFolderMethod) switch (sharedFolderMethod)
{ {
@ -330,9 +304,7 @@ public class ComfyUI : BaseGitPackage
throw new Exception("Invalid extra_model_paths.yaml"); throw new Exception("Invalid extra_model_paths.yaml");
} }
// check if we have a child called "stability_matrix" // check if we have a child called "stability_matrix"
var stabilityMatrixNode = mappingNode.Children.FirstOrDefault( var stabilityMatrixNode = mappingNode.Children.FirstOrDefault(c => c.Key.ToString() == "stability_matrix");
c => c.Key.ToString() == "stability_matrix"
);
if (stabilityMatrixNode.Key != null) if (stabilityMatrixNode.Key != null)
{ {
@ -363,10 +335,7 @@ public class ComfyUI : BaseGitPackage
{ {
{ "checkpoints", Path.Combine(modelsDir, "StableDiffusion") }, { "checkpoints", Path.Combine(modelsDir, "StableDiffusion") },
{ "vae", Path.Combine(modelsDir, "VAE") }, { "vae", Path.Combine(modelsDir, "VAE") },
{ { "loras", $"{Path.Combine(modelsDir, "Lora")}\n{Path.Combine(modelsDir, "LyCORIS")}" },
"loras",
$"{Path.Combine(modelsDir, "Lora")}\n{Path.Combine(modelsDir, "LyCORIS")}"
},
{ {
"upscale_models", "upscale_models",
$"{Path.Combine(modelsDir, "ESRGAN")}\n{Path.Combine(modelsDir, "RealESRGAN")}\n{Path.Combine(modelsDir, "SwinIR")}" $"{Path.Combine(modelsDir, "ESRGAN")}\n{Path.Combine(modelsDir, "RealESRGAN")}\n{Path.Combine(modelsDir, "SwinIR")}"
@ -383,49 +352,36 @@ public class ComfyUI : BaseGitPackage
} }
var newRootNode = new YamlMappingNode(); var newRootNode = new YamlMappingNode();
foreach ( foreach (var child in mappingNode.Children.Where(c => c.Key.ToString() != "stability_matrix"))
var child in mappingNode.Children.Where(c => c.Key.ToString() != "stability_matrix")
)
{ {
newRootNode.Children.Add(child); newRootNode.Children.Add(child);
} }
newRootNode.Children.Add(stabilityMatrixNode); newRootNode.Children.Add(stabilityMatrixNode);
var serializer = new SerializerBuilder() var serializer = new SerializerBuilder().WithNamingConvention(UnderscoredNamingConvention.Instance).Build();
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.Build();
var yamlData = serializer.Serialize(newRootNode); var yamlData = serializer.Serialize(newRootNode);
File.WriteAllText(extraPathsYamlPath, yamlData); File.WriteAllText(extraPathsYamlPath, yamlData);
return Task.CompletedTask; return Task.CompletedTask;
} }
public override Task UpdateModelFolders( public override Task UpdateModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) =>
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
) =>
sharedFolderMethod switch sharedFolderMethod switch
{ {
SharedFolderMethod.Symlink SharedFolderMethod.Symlink => base.UpdateModelFolders(installDirectory, sharedFolderMethod),
=> base.UpdateModelFolders(installDirectory, sharedFolderMethod), SharedFolderMethod.Configuration => SetupModelFolders(installDirectory, sharedFolderMethod),
SharedFolderMethod.Configuration
=> SetupModelFolders(installDirectory, sharedFolderMethod),
SharedFolderMethod.None => Task.CompletedTask, SharedFolderMethod.None => Task.CompletedTask,
_ => Task.CompletedTask _ => Task.CompletedTask
}; };
public override Task RemoveModelFolderLinks( public override Task RemoveModelFolderLinks(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod)
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
return sharedFolderMethod switch return sharedFolderMethod switch
{ {
SharedFolderMethod.Configuration => RemoveConfigSection(installDirectory), SharedFolderMethod.Configuration => RemoveConfigSection(installDirectory),
SharedFolderMethod.None => Task.CompletedTask, SharedFolderMethod.None => Task.CompletedTask,
SharedFolderMethod.Symlink SharedFolderMethod.Symlink => base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod),
=> base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod),
_ => Task.CompletedTask _ => Task.CompletedTask
}; };
} }
@ -457,9 +413,7 @@ public class ComfyUI : BaseGitPackage
mappingNode.Children.Remove("stability_matrix"); mappingNode.Children.Remove("stability_matrix");
var serializer = new SerializerBuilder() var serializer = new SerializerBuilder().WithNamingConvention(UnderscoredNamingConvention.Instance).Build();
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.Build();
var yamlData = serializer.Serialize(mappingNode); var yamlData = serializer.Serialize(mappingNode);
File.WriteAllText(extraPathsYamlPath, yamlData); File.WriteAllText(extraPathsYamlPath, yamlData);
@ -472,18 +426,13 @@ public class ComfyUI : BaseGitPackage
Action<ProcessOutput>? onConsoleOutput = null Action<ProcessOutput>? onConsoleOutput = null
) )
{ {
progress?.Report( progress?.Report(new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true));
new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true)
);
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false);
await venvRunner await venvRunner
.PipInstall( .PipInstall(
new PipInstallArgs() new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision().WithTorchExtraIndex("rocm5.6"),
.WithTorch("==2.0.1")
.WithTorchVision()
.WithTorchExtraIndex("rocm5.6"),
onConsoleOutput onConsoleOutput
) )
.ConfigureAwait(false); .ConfigureAwait(false);

20
StabilityMatrix.Core/Models/Packages/Fooocus.cs

@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
@ -26,17 +27,14 @@ public class Fooocus : BaseGitPackage
public override string DisplayName { get; set; } = "Fooocus"; public override string DisplayName { get; set; } = "Fooocus";
public override string Author => "lllyasviel"; public override string Author => "lllyasviel";
public override string Blurb => public override string Blurb => "Fooocus is a rethinking of Stable Diffusion and Midjourney’s designs";
"Fooocus is a rethinking of Stable Diffusion and Midjourney’s designs";
public override string LicenseType => "GPL-3.0"; public override string LicenseType => "GPL-3.0";
public override string LicenseUrl => "https://github.com/lllyasviel/Fooocus/blob/main/LICENSE"; public override string LicenseUrl => "https://github.com/lllyasviel/Fooocus/blob/main/LICENSE";
public override string LaunchCommand => "launch.py"; public override string LaunchCommand => "launch.py";
public override Uri PreviewImageUri => public override Uri PreviewImageUri =>
new( new("https://user-images.githubusercontent.com/19834515/261830306-f79c5981-cf80-4ee3-b06b-3fef3f8bfbc7.png");
"https://user-images.githubusercontent.com/19834515/261830306-f79c5981-cf80-4ee3-b06b-3fef3f8bfbc7.png"
);
public override List<LaunchOptionDefinition> LaunchOptions => public override List<LaunchOptionDefinition> LaunchOptions =>
new() new()
@ -79,13 +77,10 @@ public class Fooocus : BaseGitPackage
{ {
Name = "VRAM", Name = "VRAM",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
{ {
Level.Low => "--lowvram", MemoryLevel.Low => "--lowvram",
Level.Medium => "--normalvram", MemoryLevel.Medium => "--normalvram",
_ => null _ => null
}, },
Options = { "--highvram", "--normalvram", "--lowvram", "--novram" } Options = { "--highvram", "--normalvram", "--lowvram", "--novram" }
@ -158,8 +153,7 @@ public class Fooocus : BaseGitPackage
Action<ProcessOutput>? onConsoleOutput = null Action<ProcessOutput>? onConsoleOutput = null
) )
{ {
var venvRunner = await SetupVenv(installLocation, forceRecreate: true) var venvRunner = await SetupVenv(installLocation, forceRecreate: true).ConfigureAwait(false);
.ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true)); progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true));

34
StabilityMatrix.Core/Models/Packages/KohyaSs.cs

@ -4,6 +4,7 @@ using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
@ -32,11 +33,9 @@ public class KohyaSs : BaseGitPackage
public override string Name => "kohya_ss"; public override string Name => "kohya_ss";
public override string DisplayName { get; set; } = "kohya_ss"; public override string DisplayName { get; set; } = "kohya_ss";
public override string Author => "bmaltais"; public override string Author => "bmaltais";
public override string Blurb => public override string Blurb => "A Windows-focused Gradio GUI for Kohya's Stable Diffusion trainers";
"A Windows-focused Gradio GUI for Kohya's Stable Diffusion trainers";
public override string LicenseType => "Apache-2.0"; public override string LicenseType => "Apache-2.0";
public override string LicenseUrl => public override string LicenseUrl => "https://github.com/bmaltais/kohya_ss/blob/master/LICENSE.md";
"https://github.com/bmaltais/kohya_ss/blob/master/LICENSE.md";
public override string LaunchCommand => "kohya_gui.py"; public override string LaunchCommand => "kohya_gui.py";
public override Uri PreviewImageUri => public override Uri PreviewImageUri =>
@ -49,16 +48,14 @@ public class KohyaSs : BaseGitPackage
public override TorchVersion GetRecommendedTorchVersion() => TorchVersion.Cuda; public override TorchVersion GetRecommendedTorchVersion() => TorchVersion.Cuda;
public override string Disclaimer => public override string Disclaimer => "Nvidia GPU with at least 8GB VRAM is recommended. May be unstable on Linux.";
"Nvidia GPU with at least 8GB VRAM is recommended. May be unstable on Linux.";
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.UltraNightmare; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.UltraNightmare;
public override bool OfferInOneClickInstaller => false; public override bool OfferInOneClickInstaller => false;
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.None; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.None;
public override IEnumerable<TorchVersion> AvailableTorchVersions => new[] { TorchVersion.Cuda }; public override IEnumerable<TorchVersion> AvailableTorchVersions => new[] { TorchVersion.Cuda };
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods => public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods => new[] { SharedFolderMethod.None };
new[] { SharedFolderMethod.None };
public override List<LaunchOptionDefinition> LaunchOptions => public override List<LaunchOptionDefinition> LaunchOptions =>
new() new()
@ -126,9 +123,7 @@ public class KohyaSs : BaseGitPackage
{ {
if (Compat.IsWindows) if (Compat.IsWindows)
{ {
progress?.Report( progress?.Report(new ProgressReport(-1f, "Installing prerequisites...", isIndeterminate: true));
new ProgressReport(-1f, "Installing prerequisites...", isIndeterminate: true)
);
await PrerequisiteHelper.InstallTkinterIfNecessary(progress).ConfigureAwait(false); await PrerequisiteHelper.InstallTkinterIfNecessary(progress).ConfigureAwait(false);
} }
@ -203,9 +198,7 @@ public class KohyaSs : BaseGitPackage
""" """
); );
var replacementAcceleratePath = Compat.IsWindows var replacementAcceleratePath = Compat.IsWindows ? @".\venv\scripts\accelerate" : "./venv/bin/accelerate";
? @".\venv\scripts\accelerate"
: "./venv/bin/accelerate";
var replacer = scope.InvokeMethod( var replacer = scope.InvokeMethod(
"StringReplacer", "StringReplacer",
@ -259,10 +252,7 @@ public class KohyaSs : BaseGitPackage
} }
public override Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; } public override Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public override Dictionary< public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders { get; }
SharedOutputType,
IReadOnlyList<string>
>? SharedOutputFolders { get; }
public override string MainBranch => "master"; public override string MainBranch => "master";
@ -278,13 +268,7 @@ public class KohyaSs : BaseGitPackage
if (!Compat.IsWindows) if (!Compat.IsWindows)
return env; return env;
var tkPath = Path.Combine( var tkPath = Path.Combine(SettingsManager.LibraryDir, "Assets", "Python310", "tcl", "tcl8.6");
SettingsManager.LibraryDir,
"Assets",
"Python310",
"tcl",
"tcl8.6"
);
env["TCL_LIBRARY"] = tkPath; env["TCL_LIBRARY"] = tkPath;
env["TK_LIBRARY"] = tkPath; env["TK_LIBRARY"] = tkPath;

30
StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs

@ -5,6 +5,7 @@ using NLog;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
@ -22,16 +23,13 @@ public class StableDiffusionUx : BaseGitPackage
public override string DisplayName { get; set; } = "Stable Diffusion Web UI-UX"; public override string DisplayName { get; set; } = "Stable Diffusion Web UI-UX";
public override string Author => "anapnoe"; public override string Author => "anapnoe";
public override string LicenseType => "AGPL-3.0"; public override string LicenseType => "AGPL-3.0";
public override string LicenseUrl => public override string LicenseUrl => "https://github.com/anapnoe/stable-diffusion-webui-ux/blob/master/LICENSE.txt";
"https://github.com/anapnoe/stable-diffusion-webui-ux/blob/master/LICENSE.txt";
public override string Blurb => public override string Blurb =>
"A pixel perfect design, mobile friendly, customizable interface that adds accessibility, " "A pixel perfect design, mobile friendly, customizable interface that adds accessibility, "
+ "ease of use and extended functionallity to the stable diffusion web ui."; + "ease of use and extended functionallity to the stable diffusion web ui.";
public override string LaunchCommand => "launch.py"; public override string LaunchCommand => "launch.py";
public override Uri PreviewImageUri => public override Uri PreviewImageUri =>
new( new("https://raw.githubusercontent.com/anapnoe/stable-diffusion-webui-ux/master/screenshot.png");
"https://raw.githubusercontent.com/anapnoe/stable-diffusion-webui-ux/master/screenshot.png"
);
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
@ -99,13 +97,10 @@ public class StableDiffusionUx : BaseGitPackage
{ {
Name = "VRAM", Name = "VRAM",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
{ {
Level.Low => "--lowvram", MemoryLevel.Low => "--lowvram",
Level.Medium => "--medvram", MemoryLevel.Medium => "--medvram",
_ => null _ => null
}, },
Options = new() { "--lowvram", "--medvram", "--medvram-sdxl" } Options = new() { "--lowvram", "--medvram", "--medvram-sdxl" }
@ -210,9 +205,7 @@ public class StableDiffusionUx : BaseGitPackage
} }
// Install requirements file // Install requirements file
progress?.Report( progress?.Report(new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true));
new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true)
);
Logger.Info("Installing requirements_versions.txt"); Logger.Info("Installing requirements_versions.txt");
var requirements = new FilePath(installLocation, "requirements_versions.txt"); var requirements = new FilePath(installLocation, "requirements_versions.txt");
@ -259,18 +252,13 @@ public class StableDiffusionUx : BaseGitPackage
Action<ProcessOutput>? onConsoleOutput = null Action<ProcessOutput>? onConsoleOutput = null
) )
{ {
progress?.Report( progress?.Report(new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true));
new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true)
);
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false);
await venvRunner await venvRunner
.PipInstall( .PipInstall(
new PipInstallArgs() new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision().WithTorchExtraIndex("rocm5.1.1"),
.WithTorch("==2.0.1")
.WithTorchVision()
.WithTorchExtraIndex("rocm5.1.1"),
onConsoleOutput onConsoleOutput
) )
.ConfigureAwait(false); .ConfigureAwait(false);

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

@ -7,6 +7,7 @@ using NLog;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
@ -24,8 +25,7 @@ public class VladAutomatic : BaseGitPackage
public override string DisplayName { get; set; } = "SD.Next Web UI"; public override string DisplayName { get; set; } = "SD.Next Web UI";
public override string Author => "vladmandic"; public override string Author => "vladmandic";
public override string LicenseType => "AGPL-3.0"; public override string LicenseType => "AGPL-3.0";
public override string LicenseUrl => public override string LicenseUrl => "https://github.com/vladmandic/automatic/blob/master/LICENSE.txt";
"https://github.com/vladmandic/automatic/blob/master/LICENSE.txt";
public override string Blurb => "Stable Diffusion implementation with advanced features"; public override string Blurb => "Stable Diffusion implementation with advanced features";
public override string LaunchCommand => "launch.py"; public override string LaunchCommand => "launch.py";
@ -105,13 +105,10 @@ public class VladAutomatic : BaseGitPackage
{ {
Name = "VRAM", Name = "VRAM",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
{ {
Level.Low => "--lowvram", MemoryLevel.Low => "--lowvram",
Level.Medium => "--medvram", MemoryLevel.Medium => "--medvram",
_ => null _ => null
}, },
Options = new() { "--lowvram", "--medvram" } Options = new() { "--lowvram", "--medvram" }
@ -211,9 +208,7 @@ public class VladAutomatic : BaseGitPackage
break; break;
default: default:
// CPU // CPU
await venvRunner await venvRunner.CustomInstall("launch.py --debug --test", onConsoleOutput).ConfigureAwait(false);
.CustomInstall("launch.py --debug --test", onConsoleOutput)
.ConfigureAwait(false);
break; break;
} }
@ -323,20 +318,14 @@ public class VladAutomatic : BaseGitPackage
); );
await PrerequisiteHelper await PrerequisiteHelper
.RunGit( .RunGit(new[] { "checkout", versionOptions.BranchName! }, onConsoleOutput, installedPackage.FullPath)
new[] { "checkout", versionOptions.BranchName! },
onConsoleOutput,
installedPackage.FullPath
)
.ConfigureAwait(false); .ConfigureAwait(false);
var venvRunner = new PyVenvRunner(Path.Combine(installedPackage.FullPath!, "venv")); var venvRunner = new PyVenvRunner(Path.Combine(installedPackage.FullPath!, "venv"));
venvRunner.WorkingDirectory = installedPackage.FullPath!; venvRunner.WorkingDirectory = installedPackage.FullPath!;
venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables; venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables;
await venvRunner await venvRunner.CustomInstall("launch.py --upgrade --test", onConsoleOutput).ConfigureAwait(false);
.CustomInstall("launch.py --upgrade --test", onConsoleOutput)
.ConfigureAwait(false);
try try
{ {
@ -358,12 +347,7 @@ public class VladAutomatic : BaseGitPackage
finally finally
{ {
progress?.Report( progress?.Report(
new ProgressReport( new ProgressReport(1f, message: "Update Complete", isIndeterminate: false, type: ProgressType.Update)
1f,
message: "Update Complete",
isIndeterminate: false,
type: ProgressType.Update
)
); );
} }
@ -374,10 +358,7 @@ public class VladAutomatic : BaseGitPackage
}; };
} }
public override Task SetupModelFolders( public override Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod)
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
{ {
switch (sharedFolderMethod) switch (sharedFolderMethod)
{ {
@ -414,54 +395,31 @@ public class VladAutomatic : BaseGitPackage
configRoot["vae_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "VAE"); configRoot["vae_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "VAE");
configRoot["lora_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "Lora"); configRoot["lora_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "Lora");
configRoot["lyco_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "LyCORIS"); configRoot["lyco_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "LyCORIS");
configRoot["embeddings_dir"] = Path.Combine( configRoot["embeddings_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "TextualInversion");
SettingsManager.ModelsDirectory, configRoot["hypernetwork_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "Hypernetwork");
"TextualInversion" configRoot["codeformer_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "Codeformer");
);
configRoot["hypernetwork_dir"] = Path.Combine(
SettingsManager.ModelsDirectory,
"Hypernetwork"
);
configRoot["codeformer_models_path"] = Path.Combine(
SettingsManager.ModelsDirectory,
"Codeformer"
);
configRoot["gfpgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "GFPGAN"); configRoot["gfpgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "GFPGAN");
configRoot["bsrgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "BSRGAN"); configRoot["bsrgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "BSRGAN");
configRoot["esrgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "ESRGAN"); configRoot["esrgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "ESRGAN");
configRoot["realesrgan_models_path"] = Path.Combine( configRoot["realesrgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "RealESRGAN");
SettingsManager.ModelsDirectory,
"RealESRGAN"
);
configRoot["scunet_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "ScuNET"); configRoot["scunet_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "ScuNET");
configRoot["swinir_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "SwinIR"); configRoot["swinir_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "SwinIR");
configRoot["ldsr_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "LDSR"); configRoot["ldsr_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "LDSR");
configRoot["clip_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "CLIP"); configRoot["clip_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "CLIP");
configRoot["control_net_models_path"] = Path.Combine( configRoot["control_net_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "ControlNet");
SettingsManager.ModelsDirectory,
"ControlNet"
);
var configJsonStr = JsonSerializer.Serialize( var configJsonStr = JsonSerializer.Serialize(configRoot, new JsonSerializerOptions { WriteIndented = true });
configRoot,
new JsonSerializerOptions { WriteIndented = true }
);
File.WriteAllText(configJsonPath, configJsonStr); File.WriteAllText(configJsonPath, configJsonStr);
return Task.CompletedTask; return Task.CompletedTask;
} }
public override Task UpdateModelFolders( public override Task UpdateModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) =>
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
) =>
sharedFolderMethod switch sharedFolderMethod switch
{ {
SharedFolderMethod.Symlink SharedFolderMethod.Symlink => base.UpdateModelFolders(installDirectory, sharedFolderMethod),
=> base.UpdateModelFolders(installDirectory, sharedFolderMethod),
SharedFolderMethod.None => Task.CompletedTask, SharedFolderMethod.None => Task.CompletedTask,
SharedFolderMethod.Configuration SharedFolderMethod.Configuration => SetupModelFolders(installDirectory, sharedFolderMethod),
=> SetupModelFolders(installDirectory, sharedFolderMethod),
_ => Task.CompletedTask _ => Task.CompletedTask
}; };
@ -471,8 +429,7 @@ public class VladAutomatic : BaseGitPackage
) => ) =>
sharedFolderMethod switch sharedFolderMethod switch
{ {
SharedFolderMethod.Symlink SharedFolderMethod.Symlink => base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod),
=> base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod),
SharedFolderMethod.None => Task.CompletedTask, SharedFolderMethod.None => Task.CompletedTask,
SharedFolderMethod.Configuration => RemoveConfigSettings(installDirectory), SharedFolderMethod.Configuration => RemoveConfigSettings(installDirectory),
_ => Task.CompletedTask _ => Task.CompletedTask
@ -523,10 +480,7 @@ public class VladAutomatic : BaseGitPackage
configRoot.Remove("clip_models_path"); configRoot.Remove("clip_models_path");
configRoot.Remove("control_net_models_path"); configRoot.Remove("control_net_models_path");
var configJsonStr = JsonSerializer.Serialize( var configJsonStr = JsonSerializer.Serialize(configRoot, new JsonSerializerOptions { WriteIndented = true });
configRoot,
new JsonSerializerOptions { WriteIndented = true }
);
File.WriteAllText(configJsonPath, configJsonStr); File.WriteAllText(configJsonPath, configJsonStr);
return Task.CompletedTask; return Task.CompletedTask;

14
StabilityMatrix.Core/Models/Settings/Settings.cs

@ -6,7 +6,6 @@ using StabilityMatrix.Core.Models.Update;
namespace StabilityMatrix.Core.Models.Settings; namespace StabilityMatrix.Core.Models.Settings;
[JsonSerializable(typeof(Settings))]
public class Settings public class Settings
{ {
public int? Version { get; set; } = 1; public int? Version { get; set; } = 1;
@ -59,6 +58,8 @@ public class Settings
public bool IsNavExpanded { get; set; } public bool IsNavExpanded { get; set; }
public bool IsImportAsConnected { get; set; } public bool IsImportAsConnected { get; set; }
public bool ShowConnectedModelImages { get; set; } public bool ShowConnectedModelImages { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter<SharedFolderType>))]
public SharedFolderType? SharedFolderVisibleCategories { get; set; } = public SharedFolderType? SharedFolderVisibleCategories { get; set; } =
SharedFolderType.StableDiffusion | SharedFolderType.Lora | SharedFolderType.LyCORIS; SharedFolderType.StableDiffusion | SharedFolderType.Lora | SharedFolderType.LyCORIS;
@ -158,8 +159,13 @@ public class Settings
return new CultureInfo("zh-Hant"); return new CultureInfo("zh-Hant");
} }
return supportedCultures.Contains(systemCulture.Name) return supportedCultures.Contains(systemCulture.Name) ? systemCulture : new CultureInfo("en-US");
? systemCulture
: new CultureInfo("en-US");
} }
} }
[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(Settings))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
internal partial class SettingsSerializerContext : JsonSerializerContext;

113
StabilityMatrix.Core/Services/SettingsManager.cs

@ -6,7 +6,6 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using NLog; using NLog;
using Refit;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
@ -108,9 +107,7 @@ public class SettingsManager : ISettingsManager
{ {
if (!IsLibraryDirSet) if (!IsLibraryDirSet)
{ {
throw new InvalidOperationException( throw new InvalidOperationException("LibraryDir not set when BeginTransaction was called");
"LibraryDir not set when BeginTransaction was called"
);
} }
return new SettingsTransaction(this, SaveSettingsAsync); return new SettingsTransaction(this, SaveSettingsAsync);
} }
@ -136,9 +133,7 @@ public class SettingsManager : ISettingsManager
{ {
if (expression.Body is not MemberExpression memberExpression) if (expression.Body is not MemberExpression memberExpression)
{ {
throw new ArgumentException( throw new ArgumentException($"Expression must be a member expression, not {expression.Body.NodeType}");
$"Expression must be a member expression, not {expression.Body.NodeType}"
);
} }
var propertyInfo = memberExpression.Member as PropertyInfo; var propertyInfo = memberExpression.Member as PropertyInfo;
@ -189,8 +184,7 @@ public class SettingsManager : ISettingsManager
if (args.IsRelay && ReferenceEquals(sender, source)) if (args.IsRelay && ReferenceEquals(sender, source))
return; return;
Logger.Trace( Logger.Trace(
"[RelayPropertyFor] " "[RelayPropertyFor] " + "Settings.{TargetProperty:l} -> {SourceType:l}.{SourceProperty:l}",
+ "Settings.{TargetProperty:l} -> {SourceType:l}.{SourceProperty:l}",
targetPropertyName, targetPropertyName,
sourceTypeName, sourceTypeName,
propertyName propertyName
@ -206,8 +200,7 @@ public class SettingsManager : ISettingsManager
return; return;
Logger.Trace( Logger.Trace(
"[RelayPropertyFor] " "[RelayPropertyFor] " + "{SourceType:l}.{SourceProperty:l} -> Settings.{TargetProperty:l}",
+ "{SourceType:l}.{SourceProperty:l} -> Settings.{TargetProperty:l}",
sourceTypeName, sourceTypeName,
propertyName, propertyName,
targetPropertyName targetPropertyName
@ -232,10 +225,7 @@ public class SettingsManager : ISettingsManager
} }
// Invoke property changed event, passing along sender // Invoke property changed event, passing along sender
SettingsPropertyChanged?.Invoke( SettingsPropertyChanged?.Invoke(sender, new RelayPropertyChangedEventArgs(targetPropertyName, true));
sender,
new RelayPropertyChangedEventArgs(targetPropertyName, true)
);
}; };
// Set initial value if requested // Set initial value if requested
@ -276,8 +266,9 @@ public class SettingsManager : ISettingsManager
// 0. Check Override // 0. Check Override
if (!string.IsNullOrEmpty(LibraryDirOverride)) if (!string.IsNullOrEmpty(LibraryDirOverride))
{ {
Logger.Info("Using library override path: {Path}", LibraryDirOverride); var fullOverridePath = Path.GetFullPath(LibraryDirOverride);
LibraryDir = LibraryDirOverride; Logger.Info("Using library override path: {Path}", fullOverridePath);
LibraryDir = fullOverridePath;
SetStaticLibraryPaths(); SetStaticLibraryPaths();
LoadSettings(); LoadSettings();
return true; return true;
@ -339,10 +330,7 @@ public class SettingsManager : ISettingsManager
var libraryJsonFile = Compat.AppDataHome.JoinFile("library.json"); var libraryJsonFile = Compat.AppDataHome.JoinFile("library.json");
var library = new LibrarySettings { LibraryPath = path }; var library = new LibrarySettings { LibraryPath = path };
var libraryJson = JsonSerializer.Serialize( var libraryJson = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true });
library,
new JsonSerializerOptions { WriteIndented = true }
);
libraryJsonFile.WriteAllText(libraryJson); libraryJsonFile.WriteAllText(libraryJson);
// actually create the LibraryPath directory // actually create the LibraryPath directory
@ -464,9 +452,7 @@ public class SettingsManager : ISettingsManager
public void SetLastUpdateCheck(InstalledPackage package) public void SetLastUpdateCheck(InstalledPackage package)
{ {
var installedPackage = Settings.InstalledPackages.First( var installedPackage = Settings.InstalledPackages.First(p => p.DisplayName == package.DisplayName);
p => p.DisplayName == package.DisplayName
);
installedPackage.LastUpdateCheck = package.LastUpdateCheck; installedPackage.LastUpdateCheck = package.LastUpdateCheck;
installedPackage.UpdateAvailable = package.UpdateAvailable; installedPackage.UpdateAvailable = package.UpdateAvailable;
SaveSettings(); SaveSettings();
@ -494,14 +480,10 @@ public class SettingsManager : ISettingsManager
public string? GetActivePackageHost() public string? GetActivePackageHost()
{ {
var package = Settings.InstalledPackages.FirstOrDefault( var package = Settings.InstalledPackages.FirstOrDefault(x => x.Id == Settings.ActiveInstalledPackageId);
x => x.Id == Settings.ActiveInstalledPackageId
);
if (package == null) if (package == null)
return null; return null;
var hostOption = package.LaunchArgs?.FirstOrDefault( var hostOption = package.LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "host");
x => x.Name.ToLowerInvariant() == "host"
);
if (hostOption?.OptionValue != null) if (hostOption?.OptionValue != null)
{ {
return hostOption.OptionValue as string; return hostOption.OptionValue as string;
@ -511,14 +493,10 @@ public class SettingsManager : ISettingsManager
public string? GetActivePackagePort() public string? GetActivePackagePort()
{ {
var package = Settings.InstalledPackages.FirstOrDefault( var package = Settings.InstalledPackages.FirstOrDefault(x => x.Id == Settings.ActiveInstalledPackageId);
x => x.Id == Settings.ActiveInstalledPackageId
);
if (package == null) if (package == null)
return null; return null;
var portOption = package.LaunchArgs?.FirstOrDefault( var portOption = package.LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "port");
x => x.Name.ToLowerInvariant() == "port"
);
if (portOption?.OptionValue != null) if (portOption?.OptionValue != null)
{ {
return portOption.OptionValue as string; return portOption.OptionValue as string;
@ -616,23 +594,34 @@ public class SettingsManager : ISettingsManager
FileLock.EnterReadLock(); FileLock.EnterReadLock();
try try
{ {
if (!File.Exists(SettingsPath)) var settingsFile = new FilePath(SettingsPath);
if (!settingsFile.Exists)
{
settingsFile.Directory?.Create();
settingsFile.Create();
var settingsJson = JsonSerializer.Serialize(Settings);
settingsFile.WriteAllText(settingsJson);
Loaded?.Invoke(this, EventArgs.Empty);
return;
}
using var fileStream = settingsFile.Info.OpenRead();
if (fileStream.Length == 0)
{ {
File.Create(SettingsPath).Close(); Logger.Warn("Settings file is empty, using default settings");
Settings.Theme = "Dark";
var defaultSettingsJson = JsonSerializer.Serialize(Settings);
File.WriteAllText(SettingsPath, defaultSettingsJson);
return; return;
} }
var settingsContent = File.ReadAllText(SettingsPath); if (
var modifiedDefaultSerializerOptions = JsonSerializer.Deserialize(fileStream, SettingsSerializerContext.Default.Settings) is { } loadedSettings
SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions(); )
modifiedDefaultSerializerOptions.Converters.Add(new JsonStringEnumConverter()); {
Settings = JsonSerializer.Deserialize<Settings>( Settings = loadedSettings;
settingsContent, }
modifiedDefaultSerializerOptions
)!;
Loaded?.Invoke(this, EventArgs.Empty); Loaded?.Invoke(this, EventArgs.Empty);
} }
@ -644,24 +633,20 @@ public class SettingsManager : ISettingsManager
protected virtual void SaveSettings() protected virtual void SaveSettings()
{ {
FileLock.TryEnterWriteLock(100000); FileLock.TryEnterWriteLock(TimeSpan.FromSeconds(30));
try try
{ {
if (!File.Exists(SettingsPath)) var settingsFile = new FilePath(SettingsPath);
{
File.Create(SettingsPath).Close();
}
var json = JsonSerializer.Serialize( if (!settingsFile.Exists)
Settings,
new JsonSerializerOptions
{ {
WriteIndented = true, settingsFile.Directory?.Create();
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, settingsFile.Create();
Converters = { new JsonStringEnumConverter() }
} }
);
File.WriteAllText(SettingsPath, json); var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(Settings, SettingsSerializerContext.Default.Settings);
File.WriteAllBytes(SettingsPath, jsonBytes);
} }
finally finally
{ {
@ -693,9 +678,9 @@ public class SettingsManager : ISettingsManager
{ {
try try
{ {
await Task.Delay(delay, cts.Token); await Task.Delay(delay, cts.Token).ConfigureAwait(false);
await SaveSettingsAsync(); await SaveSettingsAsync().ConfigureAwait(false);
} }
catch (TaskCanceledException) { } catch (TaskCanceledException) { }
finally finally

2
StabilityMatrix.Core/StabilityMatrix.Core.csproj

@ -7,6 +7,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<EnableWindowsTargeting>true</EnableWindowsTargeting> <EnableWindowsTargeting>true</EnableWindowsTargeting>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -25,6 +26,7 @@
<PackageReference Include="DeviceId.Windows" Version="6.5.0" /> <PackageReference Include="DeviceId.Windows" Version="6.5.0" />
<PackageReference Include="DeviceId.Windows.Wmi" Version="6.5.1" /> <PackageReference Include="DeviceId.Windows.Wmi" Version="6.5.1" />
<PackageReference Include="DynamicData" Version="8.1.1" /> <PackageReference Include="DynamicData" Version="8.1.1" />
<PackageReference Include="Hardware.Info" Version="100.0.0.1" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" /> <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="LiteDB" Version="5.0.17" /> <PackageReference Include="LiteDB" Version="5.0.17" />
<PackageReference Include="LiteDB.Async" Version="0.1.7" /> <PackageReference Include="LiteDB.Async" Version="0.1.7" />

35
StabilityMatrix.Tests/Core/DefaultUnknownEnumConverterTests.cs

@ -1,4 +1,5 @@
using System.Text.Json; using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Converters.Json;
@ -46,6 +47,16 @@ public class DefaultUnknownEnumConverterTests
Assert.AreEqual(expected, result); Assert.AreEqual(expected, result);
} }
[TestMethod]
public void TestDeserialize_UnknownEnum_ShouldUseEnumMemberValue()
{
const string json = "\"Value 2\"";
var result = JsonSerializer.Deserialize<UnknownEnum>(json);
Assert.AreEqual(UnknownEnum.Value2, result);
}
[TestMethod] [TestMethod]
public void TestSerialize_DefaultEnum_ShouldConvert() public void TestSerialize_DefaultEnum_ShouldConvert()
{ {
@ -56,6 +67,26 @@ public class DefaultUnknownEnumConverterTests
Assert.AreEqual(expected, result); Assert.AreEqual(expected, result);
} }
[TestMethod]
public void TestSerialize_UnknownEnum_ShouldUseEnumMemberValue()
{
const string json = "\"Value 2\"";
var result = JsonSerializer.Deserialize<UnknownEnum>(json);
Assert.AreEqual(UnknownEnum.Value2, result);
}
[TestMethod]
public void TestSerialize_ComplexObject_ShouldUseEnumMemberValue()
{
const string expected = "{\"Key\":\"Value 2\"}";
var result = JsonSerializer.Serialize(new { Key = UnknownEnum.Value2 });
Assert.AreEqual(expected, result);
}
private enum NormalEnum private enum NormalEnum
{ {
Unknown, Unknown,
@ -68,6 +99,8 @@ public class DefaultUnknownEnumConverterTests
{ {
Unknown, Unknown,
Value1, Value1,
[EnumMember(Value = "Value 2")]
Value2 Value2
} }

Loading…
Cancel
Save