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": {
"version": "0.25.0",
"version": "0.26.3",
"commands": [
"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/),
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
### Fixed
- 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" />
<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="ToolTipForeground" Color="#9FBDC3"/>
<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
# noinspection PyUnresolvedReferences
sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
# Install the audit hook
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;
}
protected bool RaiseAndSetIfChanged<T>(
ref T field,
T value,
[CallerMemberName] string? propertyName = null
)
protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
@ -335,9 +331,7 @@ public class AdvancedImageBox : TemplatedControl
if (index < Count - 1)
index++;
return constrainZoomLevel > 0 && this[index] >= constrainZoomLevel
? constrainZoomLevel
: this[index];
return constrainZoomLevel > 0 && this[index] >= constrainZoomLevel ? constrainZoomLevel : this[index];
}
/// <summary>
@ -352,9 +346,7 @@ public class AdvancedImageBox : TemplatedControl
if (index > 0)
index--;
return constrainZoomLevel > 0 && this[index] <= constrainZoomLevel
? constrainZoomLevel
: this[index];
return constrainZoomLevel > 0 && this[index] <= constrainZoomLevel ? constrainZoomLevel : this[index];
}
/// <summary>
@ -525,11 +517,10 @@ public class AdvancedImageBox : TemplatedControl
#endregion
#region Properties
public static readonly DirectProperty<AdvancedImageBox, bool> CanRenderProperty =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>(
nameof(CanRender),
o => o.CanRender
);
public static readonly DirectProperty<AdvancedImageBox, bool> CanRenderProperty = AvaloniaProperty.RegisterDirect<
AdvancedImageBox,
bool
>(nameof(CanRender), o => o.CanRender);
/// <summary>
/// Gets or sets if control can render the image
@ -560,11 +551,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(GridCellSizeProperty, value);
}
public static readonly StyledProperty<ISolidColorBrush> GridColorProperty =
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(
nameof(GridColor),
SolidColorBrush.Parse("#181818")
);
public static readonly StyledProperty<ISolidColorBrush> GridColorProperty = AvaloniaProperty.Register<
AdvancedImageBox,
ISolidColorBrush
>(nameof(GridColor), SolidColorBrush.Parse("#181818"));
/// <summary>
/// Gets or sets the color used to create the checkerboard style background
@ -575,11 +565,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(GridColorProperty, value);
}
public static readonly StyledProperty<ISolidColorBrush> GridColorAlternateProperty =
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(
nameof(GridColorAlternate),
SolidColorBrush.Parse("#252525")
);
public static readonly StyledProperty<ISolidColorBrush> GridColorAlternateProperty = AvaloniaProperty.Register<
AdvancedImageBox,
ISolidColorBrush
>(nameof(GridColorAlternate), SolidColorBrush.Parse("#252525"));
/// <summary>
/// Gets or sets the color used to create the checkerboard style background
@ -606,7 +595,8 @@ public class AdvancedImageBox : TemplatedControl
{
var loader = ImageLoader.AsyncImageLoader;
Dispatcher.UIThread
Dispatcher
.UIThread
.InvokeAsync(async () =>
{
Image = await loader.ProvideImageAsync(value);
@ -616,10 +606,9 @@ public class AdvancedImageBox : TemplatedControl
}
}
public static readonly StyledProperty<Bitmap?> ImageProperty = AvaloniaProperty.Register<
AdvancedImageBox,
Bitmap?
>(nameof(Image));
public static readonly StyledProperty<Bitmap?> ImageProperty = AvaloniaProperty.Register<AdvancedImageBox, Bitmap?>(
nameof(Image)
);
/// <summary>
/// Gets or sets the image to be displayed
@ -656,8 +645,7 @@ public class AdvancedImageBox : TemplatedControl
{
offsetBackup = Offset;
var zoomFactorScale =
(float)GetZoomLevelToFit(newImage) / GetZoomLevelToFit(oldImage);
var zoomFactorScale = (float)GetZoomLevelToFit(newImage) / GetZoomLevelToFit(oldImage);
var imageScale = newImage.Size / oldImage.Size;
Debug.WriteLine($"Image scale: {imageScale}");
@ -731,8 +719,10 @@ public class AdvancedImageBox : TemplatedControl
public bool HaveTrackerImage => _trackerImage is not null;
public static readonly StyledProperty<bool> TrackerImageAutoZoomProperty =
AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(TrackerImageAutoZoom), true);
public static readonly StyledProperty<bool> TrackerImageAutoZoomProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>(nameof(TrackerImageAutoZoom), true);
/// <summary>
/// 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);
}
public static readonly StyledProperty<bool> IsTrackerImageEnabledProperty =
AvaloniaProperty.Register<AdvancedImageBox, bool>("IsTrackerImageEnabled");
public static readonly StyledProperty<bool> IsTrackerImageEnabledProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>("IsTrackerImageEnabled");
public bool IsTrackerImageEnabled
{
@ -776,10 +768,10 @@ public class AdvancedImageBox : TemplatedControl
}
}
public static readonly StyledProperty<bool> ShowGridProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>(nameof(ShowGrid), true);
public static readonly StyledProperty<bool> ShowGridProperty = AvaloniaProperty.Register<AdvancedImageBox, bool>(
nameof(ShowGrid),
true
);
/// <summary>
/// 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 =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, Point>(
nameof(PointerPosition),
o => o.PointerPosition
);
AvaloniaProperty.RegisterDirect<AdvancedImageBox, Point>(nameof(PointerPosition), o => o.PointerPosition);
/// <summary>
/// Gets the current pointer position
@ -805,11 +794,10 @@ public class AdvancedImageBox : TemplatedControl
private set => SetAndRaise(PointerPositionProperty, ref _pointerPosition, value);
}
public static readonly DirectProperty<AdvancedImageBox, bool> IsPanningProperty =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>(
nameof(IsPanning),
o => o.IsPanning
);
public static readonly DirectProperty<AdvancedImageBox, bool> IsPanningProperty = AvaloniaProperty.RegisterDirect<
AdvancedImageBox,
bool
>(nameof(IsPanning), o => o.IsPanning);
/// <summary>
/// Gets if control is currently panning
@ -836,11 +824,10 @@ public class AdvancedImageBox : TemplatedControl
}
}
public static readonly DirectProperty<AdvancedImageBox, bool> IsSelectingProperty =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, bool>(
nameof(IsSelecting),
o => o.IsSelecting
);
public static readonly DirectProperty<AdvancedImageBox, bool> IsSelectingProperty = AvaloniaProperty.RegisterDirect<
AdvancedImageBox,
bool
>(nameof(IsSelecting), o => o.IsSelecting);
/// <summary>
/// Gets if control is currently selecting a ROI
@ -863,10 +850,10 @@ public class AdvancedImageBox : TemplatedControl
}
}
public static readonly StyledProperty<bool> AutoPanProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>(nameof(AutoPan), true);
public static readonly StyledProperty<bool> AutoPanProperty = AvaloniaProperty.Register<AdvancedImageBox, bool>(
nameof(AutoPan),
true
);
/// <summary>
/// Gets or sets if the control can pan with the mouse
@ -877,11 +864,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(AutoPanProperty, value);
}
public static readonly StyledProperty<MouseButtons> PanWithMouseButtonsProperty =
AvaloniaProperty.Register<AdvancedImageBox, MouseButtons>(
nameof(PanWithMouseButtons),
MouseButtons.LeftButton | MouseButtons.MiddleButton
);
public static readonly StyledProperty<MouseButtons> PanWithMouseButtonsProperty = AvaloniaProperty.Register<
AdvancedImageBox,
MouseButtons
>(nameof(PanWithMouseButtons), MouseButtons.LeftButton | MouseButtons.MiddleButton);
/// <summary>
/// Gets or sets the mouse buttons to pan the image
@ -906,11 +892,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(PanWithArrowsProperty, value);
}
public static readonly StyledProperty<MouseButtons> SelectWithMouseButtonsProperty =
AvaloniaProperty.Register<AdvancedImageBox, MouseButtons>(
nameof(SelectWithMouseButtons),
MouseButtons.LeftButton
);
public static readonly StyledProperty<MouseButtons> SelectWithMouseButtonsProperty = AvaloniaProperty.Register<
AdvancedImageBox,
MouseButtons
>(nameof(SelectWithMouseButtons), MouseButtons.LeftButton);
/// <summary>
/// Gets or sets the mouse buttons to select a region on image
@ -935,10 +920,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(InvertMousePanProperty, value);
}
public static readonly StyledProperty<bool> AutoCenterProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>(nameof(AutoCenter), true);
public static readonly StyledProperty<bool> AutoCenterProperty = AvaloniaProperty.Register<AdvancedImageBox, bool>(
nameof(AutoCenter),
true
);
/// <summary>
/// Gets or sets if image is auto centered
@ -993,10 +978,10 @@ public class AdvancedImageBox : TemplatedControl
}
}
public static readonly StyledProperty<bool> AllowZoomProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>(nameof(AllowZoom), true);
public static readonly StyledProperty<bool> AllowZoomProperty = AvaloniaProperty.Register<AdvancedImageBox, bool>(
nameof(AllowZoom),
true
);
/// <summary>
/// Gets or sets if zoom is allowed
@ -1007,10 +992,8 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(AllowZoomProperty, value);
}
public static readonly DirectProperty<
AdvancedImageBox,
ZoomLevelCollection
> ZoomLevelsProperty = AvaloniaProperty.RegisterDirect<AdvancedImageBox, ZoomLevelCollection>(
public static readonly DirectProperty<AdvancedImageBox, ZoomLevelCollection> ZoomLevelsProperty =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, ZoomLevelCollection>(
nameof(ZoomLevels),
o => o.ZoomLevels,
(o, v) => o.ZoomLevels = v
@ -1028,10 +1011,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetAndRaise(ZoomLevelsProperty, ref _zoomLevels, value);
}
public static readonly StyledProperty<int> MinZoomProperty = AvaloniaProperty.Register<
AdvancedImageBox,
int
>(nameof(MinZoom), 10);
public static readonly StyledProperty<int> MinZoomProperty = AvaloniaProperty.Register<AdvancedImageBox, int>(
nameof(MinZoom),
10
);
/// <summary>
/// Gets or sets the minimum possible zoom.
@ -1043,10 +1026,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(MinZoomProperty, value);
}
public static readonly StyledProperty<int> MaxZoomProperty = AvaloniaProperty.Register<
AdvancedImageBox,
int
>(nameof(MaxZoom), 6400);
public static readonly StyledProperty<int> MaxZoomProperty = AvaloniaProperty.Register<AdvancedImageBox, int>(
nameof(MaxZoom),
6400
);
/// <summary>
/// Gets or sets the maximum possible zoom.
@ -1058,8 +1041,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(MaxZoomProperty, value);
}
public static readonly StyledProperty<bool> ConstrainZoomOutToFitLevelProperty =
AvaloniaProperty.Register<AdvancedImageBox, bool>(nameof(ConstrainZoomOutToFitLevel), true);
public static readonly StyledProperty<bool> ConstrainZoomOutToFitLevelProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>(nameof(ConstrainZoomOutToFitLevel), true);
/// <summary>
/// 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);
}
public static readonly DirectProperty<AdvancedImageBox, int> OldZoomProperty =
AvaloniaProperty.RegisterDirect<AdvancedImageBox, int>(nameof(OldZoom), o => o.OldZoom);
public static readonly DirectProperty<AdvancedImageBox, int> OldZoomProperty = AvaloniaProperty.RegisterDirect<
AdvancedImageBox,
int
>(nameof(OldZoom), o => o.OldZoom);
private int _oldZoom = 100;
@ -1085,10 +1072,10 @@ public class AdvancedImageBox : TemplatedControl
private set => SetAndRaise(OldZoomProperty, ref _oldZoom, value);
}
public static readonly StyledProperty<int> ZoomProperty = AvaloniaProperty.Register<
AdvancedImageBox,
int
>(nameof(Zoom), 100);
public static readonly StyledProperty<int> ZoomProperty = AvaloniaProperty.Register<AdvancedImageBox, int>(
nameof(Zoom),
100
);
/// <summary>
/// Gets or sets the zoom.
@ -1178,11 +1165,10 @@ public class AdvancedImageBox : TemplatedControl
/// <value>The height of the scaled image.</value>
public double ScaledImageHeight => Image?.Size.Height * ZoomFactor ?? 0;
public static readonly StyledProperty<ISolidColorBrush> PixelGridColorProperty =
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(
nameof(PixelGridColor),
Brushes.DimGray
);
public static readonly StyledProperty<ISolidColorBrush> PixelGridColorProperty = AvaloniaProperty.Register<
AdvancedImageBox,
ISolidColorBrush
>(nameof(PixelGridColor), Brushes.DimGray);
/// <summary>
/// Gets or sets the color of the pixel grid.
@ -1194,8 +1180,10 @@ public class AdvancedImageBox : TemplatedControl
set => SetValue(PixelGridColorProperty, value);
}
public static readonly StyledProperty<int> PixelGridZoomThresholdProperty =
AvaloniaProperty.Register<AdvancedImageBox, int>(nameof(PixelGridZoomThreshold), 13);
public static readonly StyledProperty<int> PixelGridZoomThresholdProperty = AvaloniaProperty.Register<
AdvancedImageBox,
int
>(nameof(PixelGridZoomThreshold), 13);
/// <summary>
/// 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);
}
public static readonly StyledProperty<SelectionModes> SelectionModeProperty =
AvaloniaProperty.Register<AdvancedImageBox, SelectionModes>(nameof(SelectionMode));
public static readonly StyledProperty<SelectionModes> SelectionModeProperty = AvaloniaProperty.Register<
AdvancedImageBox,
SelectionModes
>(nameof(SelectionMode));
public static readonly StyledProperty<bool> IsPixelGridEnabledProperty =
AvaloniaProperty.Register<AdvancedImageBox, bool>("IsPixelGridEnabled", true);
public static readonly StyledProperty<bool> IsPixelGridEnabledProperty = AvaloniaProperty.Register<
AdvancedImageBox,
bool
>("IsPixelGridEnabled", true);
/// <summary>
/// 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);
}
public static readonly StyledProperty<ISolidColorBrush> SelectionColorProperty =
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(
nameof(SelectionColor),
new SolidColorBrush(new Color(127, 0, 128, 255))
);
public static readonly StyledProperty<ISolidColorBrush> SelectionColorProperty = AvaloniaProperty.Register<
AdvancedImageBox,
ISolidColorBrush
>(nameof(SelectionColor), new SolidColorBrush(new Color(127, 0, 128, 255)));
public ISolidColorBrush SelectionColor
{
@ -1313,14 +1304,9 @@ public class AdvancedImageBox : TemplatedControl
AffectsRender<AdvancedImageBox>(ShowGridProperty);
HorizontalScrollBar =
e.NameScope.Find<ScrollBar>("PART_HorizontalScrollBar")
?? throw new NullReferenceException();
VerticalScrollBar =
e.NameScope.Find<ScrollBar>("PART_VerticalScrollBar")
?? throw new NullReferenceException();
ViewPort =
e.NameScope.Find<ContentPresenter>("PART_ViewPort")
?? throw new NullReferenceException();
e.NameScope.Find<ScrollBar>("PART_HorizontalScrollBar") ?? throw new NullReferenceException();
VerticalScrollBar = e.NameScope.Find<ScrollBar>("PART_VerticalScrollBar") ?? throw new NullReferenceException();
ViewPort = e.NameScope.Find<ContentPresenter>("PART_ViewPort") ?? throw new NullReferenceException();
SizeModeChanged();
@ -1347,13 +1333,12 @@ public class AdvancedImageBox : TemplatedControl
// If we're in high zoom, switch off bitmap interpolation mode
// Otherwise use high quality
BitmapInterpolationMode = isHighZoom
? BitmapInterpolationMode.None
: BitmapInterpolationMode.HighQuality;
BitmapInterpolationMode = isHighZoom ? BitmapInterpolationMode.None : BitmapInterpolationMode.HighQuality;
InvalidateVisual();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RenderBackgroundGrid(DrawingContext context)
{
var size = GridCellSize;
@ -1387,8 +1372,6 @@ public class AdvancedImageBox : TemplatedControl
public override void Render(DrawingContext context)
{
base.Render(context);
var gridCellSize = GridCellSize;
if (ShowGrid & gridCellSize > 0 && (!IsHorizontalBarVisible || !IsVerticalBarVisible))
@ -1399,9 +1382,7 @@ public class AdvancedImageBox : TemplatedControl
var zoomFactor = ZoomFactor;
var shouldDrawPixelGrid =
IsPixelGridEnabled
&& SizeMode == SizeModes.Normal
&& zoomFactor > PixelGridZoomThreshold;
IsPixelGridEnabled && SizeMode == SizeModes.Normal && zoomFactor > PixelGridZoomThreshold;
// Draw Grid
/*var viewPortSize = ViewPortSize;
@ -1441,16 +1422,10 @@ public class AdvancedImageBox : TemplatedControl
if (HaveTrackerImage && _pointerPosition is { X: >= 0, Y: >= 0 })
{
var destSize = TrackerImageAutoZoom
? new Size(
_trackerImage!.Size.Width * zoomFactor,
_trackerImage.Size.Height * zoomFactor
)
? new Size(_trackerImage!.Size.Width * zoomFactor, _trackerImage.Size.Height * zoomFactor)
: image.Size;
var destPos = new Point(
_pointerPosition.X - destSize.Width / 2,
_pointerPosition.Y - destSize.Height / 2
);
var destPos = new Point(_pointerPosition.X - destSize.Width / 2, _pointerPosition.Y - destSize.Height / 2);
context.DrawImage(_trackerImage!, new Rect(destPos, destSize));
}
@ -1462,30 +1437,14 @@ public class AdvancedImageBox : TemplatedControl
var offsetY = Offset.Y % zoomFactor;
Pen pen = new(PixelGridColor);
for (
var x = imageViewPort.X + zoomFactor - offsetX;
x < imageViewPort.Right;
x += zoomFactor
)
for (var x = imageViewPort.X + zoomFactor - offsetX; x < imageViewPort.Right; x += zoomFactor)
{
context.DrawLine(
pen,
new Point(x, imageViewPort.X),
new Point(x, imageViewPort.Bottom)
);
context.DrawLine(pen, new Point(x, imageViewPort.X), new Point(x, imageViewPort.Bottom));
}
for (
var y = imageViewPort.Y + zoomFactor - offsetY;
y < imageViewPort.Bottom;
y += zoomFactor
)
for (var y = imageViewPort.Y + zoomFactor - offsetY; y < imageViewPort.Bottom; y += zoomFactor)
{
context.DrawLine(
pen,
new Point(imageViewPort.Y, y),
new Point(imageViewPort.Right, y)
);
context.DrawLine(pen, new Point(imageViewPort.Y, y), new Point(imageViewPort.Right, y));
}
context.DrawRectangle(pen, imageViewPort);
@ -1496,12 +1455,7 @@ public class AdvancedImageBox : TemplatedControl
var rect = GetOffsetRectangle(SelectionRegion);
var selectionColor = SelectionColor;
context.FillRectangle(selectionColor, rect);
var color = Color.FromArgb(
255,
selectionColor.Color.R,
selectionColor.Color.G,
selectionColor.Color.B
);
var color = Color.FromArgb(255, selectionColor.Color.R, selectionColor.Color.G, selectionColor.Color.B);
context.DrawRectangle(new Pen(color.ToUInt32()), rect);
}
}
@ -1609,8 +1563,7 @@ public class AdvancedImageBox : TemplatedControl
{
if (
!(
pointer.Properties.IsLeftButtonPressed
&& (SelectWithMouseButtons & MouseButtons.LeftButton) != 0
pointer.Properties.IsLeftButtonPressed && (SelectWithMouseButtons & MouseButtons.LeftButton) != 0
|| pointer.Properties.IsMiddleButtonPressed
&& (SelectWithMouseButtons & MouseButtons.MiddleButton) != 0
|| pointer.Properties.IsRightButtonPressed
@ -1624,12 +1577,10 @@ public class AdvancedImageBox : TemplatedControl
{
if (
!(
pointer.Properties.IsLeftButtonPressed
&& (PanWithMouseButtons & MouseButtons.LeftButton) != 0
pointer.Properties.IsLeftButtonPressed && (PanWithMouseButtons & MouseButtons.LeftButton) != 0
|| pointer.Properties.IsMiddleButtonPressed
&& (PanWithMouseButtons & MouseButtons.MiddleButton) != 0
|| pointer.Properties.IsRightButtonPressed
&& (PanWithMouseButtons & MouseButtons.RightButton) != 0
|| pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & MouseButtons.RightButton) != 0
)
|| !AutoPan
|| SizeMode != SizeModes.Normal
@ -2669,12 +2620,8 @@ public class AdvancedImageBox : TemplatedControl
case SizeModes.Normal:
if (AutoCenter)
{
xOffset = (
!IsHorizontalBarVisible ? (viewPortSize.Width - ScaledImageWidth) / 2 : 0
);
yOffset = (
!IsVerticalBarVisible ? (viewPortSize.Height - ScaledImageHeight) / 2 : 0
);
xOffset = (!IsHorizontalBarVisible ? (viewPortSize.Width - ScaledImageWidth) / 2 : 0);
yOffset = (!IsVerticalBarVisible ? (viewPortSize.Height - ScaledImageHeight) / 2 : 0);
}
width = Math.Min(ScaledImageWidth - Math.Abs(Offset.X), viewPortSize.Width);
@ -2729,12 +2676,7 @@ public class AdvancedImageBox : TemplatedControl
var pixelSize = SelectionPixelSize;
using var frameBuffer = image.Lock();
var newBitmap = new WriteableBitmap(
pixelSize,
image.Dpi,
frameBuffer.Format,
AlphaFormat.Unpremul
);
var newBitmap = new WriteableBitmap(pixelSize, image.Dpi, frameBuffer.Format, AlphaFormat.Unpremul);
using var newFrameBuffer = newBitmap.Lock();
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
[NotNull]
private static readonly FieldInfo? IsCornerRadiusUsedField = typeof(AdvancedImage).GetField(
"_isCornerRadiusUsed",BindingFlags.Instance | BindingFlags.NonPublic);
"_isCornerRadiusUsed",
BindingFlags.Instance | BindingFlags.NonPublic
);
[NotNull]
private static readonly FieldInfo? CornerRadiusClipField = typeof(AdvancedImage).GetField(
"_cornerRadiusClip",BindingFlags.Instance | BindingFlags.NonPublic);
"_cornerRadiusClip",
BindingFlags.Instance | BindingFlags.NonPublic
);
private bool IsCornerRadiusUsed
{
@ -48,13 +52,11 @@ public class BetterAdvancedImage : 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>
/// <inheritdoc />
@ -77,7 +79,8 @@ public class BetterAdvancedImage : AdvancedImage
HorizontalAlignment.Center => (int)(viewPort.Width - scaledSize.Width) / 2,
HorizontalAlignment.Right => (int)(viewPort.Width - scaledSize.Width),
// 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))
};
var destY = VerticalContentAlignment switch
@ -89,11 +92,7 @@ public class BetterAdvancedImage : AdvancedImage
_ => throw new ArgumentException(nameof(VerticalContentAlignment))
};
var destRect = viewPort
.CenterRect(new Rect(scaledSize))
.WithX(destX)
.WithY(destY)
.Intersect(viewPort);
var destRect = viewPort.CenterRect(new Rect(scaledSize)).WithX(destX).WithY(destY).Intersect(viewPort);
var destRectUnscaledSize = destRect.Size / scale;
// Calculate starting points for source
@ -103,7 +102,8 @@ public class BetterAdvancedImage : AdvancedImage
HorizontalAlignment.Center => (int)(sourceSize - destRectUnscaledSize).Width / 2,
HorizontalAlignment.Right => (int)(sourceSize - destRectUnscaledSize).Width,
// Stretch is default, use center
HorizontalAlignment.Stretch => (int) (sourceSize - destRectUnscaledSize).Width / 2,
HorizontalAlignment.Stretch
=> (int)(sourceSize - destRectUnscaledSize).Width / 2,
_ => throw new ArgumentException(nameof(HorizontalContentAlignment))
};
var sourceY = VerticalContentAlignment switch
@ -120,10 +120,17 @@ public class BetterAdvancedImage : AdvancedImage
.WithX(sourceX)
.WithY(sourceY);
DrawingContext.PushedState? pushedState =
IsCornerRadiusUsed ? context.PushClip(CornerRadiusClip) : null;
if (IsCornerRadiusUsed)
{
using (context.PushClip(CornerRadiusClip))
{
context.DrawImage(source, sourceRect, destRect);
pushedState?.Dispose();
}
}
else
{
context.DrawImage(source, sourceRect, destRect);
}
}
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);
public static readonly StyledProperty<ISolidColorBrush> StrokeProperty =
AvaloniaProperty.Register<LineDashFrame, ISolidColorBrush>("Stroke");
public static readonly StyledProperty<ISolidColorBrush> StrokeProperty = AvaloniaProperty.Register<
LineDashFrame,
ISolidColorBrush
>("Stroke");
public ISolidColorBrush Stroke
{
@ -21,8 +23,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeProperty, value);
}
public static readonly StyledProperty<double> StrokeThicknessProperty =
AvaloniaProperty.Register<LineDashFrame, double>("StrokeThickness");
public static readonly StyledProperty<double> StrokeThicknessProperty = AvaloniaProperty.Register<
LineDashFrame,
double
>("StrokeThickness");
public double StrokeThickness
{
@ -30,8 +34,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeThicknessProperty, value);
}
public static readonly StyledProperty<double> StrokeDashLineProperty =
AvaloniaProperty.Register<LineDashFrame, double>("StrokeDashLine");
public static readonly StyledProperty<double> StrokeDashLineProperty = AvaloniaProperty.Register<
LineDashFrame,
double
>("StrokeDashLine");
public double StrokeDashLine
{
@ -39,8 +45,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeDashLineProperty, value);
}
public static readonly StyledProperty<double> StrokeDashSpaceProperty =
AvaloniaProperty.Register<LineDashFrame, double>("StrokeDashSpace");
public static readonly StyledProperty<double> StrokeDashSpaceProperty = AvaloniaProperty.Register<
LineDashFrame,
double
>("StrokeDashSpace");
public double StrokeDashSpace
{
@ -48,8 +56,10 @@ public class LineDashFrame : Frame
set => SetValue(StrokeDashSpaceProperty, value);
}
public static readonly StyledProperty<ISolidColorBrush> FillProperty =
AvaloniaProperty.Register<LineDashFrame, ISolidColorBrush>("Fill");
public static readonly StyledProperty<ISolidColorBrush> FillProperty = AvaloniaProperty.Register<
LineDashFrame,
ISolidColorBrush
>("Fill");
public ISolidColorBrush Fill
{
@ -82,17 +92,12 @@ public class LineDashFrame : Frame
/// <inheritdoc />
public override void Render(DrawingContext context)
{
base.Render(context);
var width = Bounds.Width;
var height = Bounds.Height;
context.DrawRectangle(Fill, null, new Rect(0, 0, width, height));
var dashPen = new Pen(Stroke, StrokeThickness)
{
DashStyle = new DashStyle(GetDashArray(width), 0)
};
var dashPen = new Pen(Stroke, StrokeThickness) { DashStyle = new DashStyle(GetDashArray(width), 0) };
context.DrawLine(dashPen, new Point(0, 0), new Point(width, 0));
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.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
namespace StabilityMatrix.Avalonia.Controls;
@ -13,34 +15,11 @@ namespace StabilityMatrix.Avalonia.Controls;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class ProgressRing : RangeBase
{
public static readonly StyledProperty<bool> IsIndeterminateProperty =
ProgressBar.IsIndeterminateProperty.AddOwner<ProgressRing>();
private Arc? fillArc;
public static readonly StyledProperty<bool> PreserveAspectProperty =
AvaloniaProperty.Register<ProgressRing, bool>(nameof(PreserveAspect), true);
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 static readonly StyledProperty<bool> IsIndeterminateProperty = ProgressBar
.IsIndeterminateProperty
.AddOwner<ProgressRing>();
public bool IsIndeterminate
{
@ -48,35 +27,90 @@ public class ProgressRing : RangeBase
set => SetValue(IsIndeterminateProperty, value);
}
public static readonly StyledProperty<bool> PreserveAspectProperty = AvaloniaProperty.Register<ProgressRing, bool>(
nameof(PreserveAspect),
true
);
public bool PreserveAspect
{
get => GetValue(PreserveAspectProperty);
set => SetValue(PreserveAspectProperty, value);
}
public double ValueAngle
public static readonly StyledProperty<double> StrokeThicknessProperty = Shape
.StrokeThicknessProperty
.AddOwner<ProgressRing>();
public double StrokeThickness
{
get => GetValue(ValueAngleProperty);
private set => SetValue(ValueAngleProperty, value);
get => GetValue(StrokeThicknessProperty);
set => SetValue(StrokeThicknessProperty, value);
}
public static readonly StyledProperty<double> StartAngleProperty = AvaloniaProperty.Register<ProgressRing, double>(
nameof(StartAngle)
);
public double StartAngle
{
get => GetValue(StartAngleProperty);
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
{
get => GetValue(EndAngleProperty);
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)
{
base.OnPropertyChanged(change);
var e = change as AvaloniaPropertyChangedEventArgs<bool>;
if (e is null) return;
if (e is null)
return;
if (e.Property == IsIndeterminateProperty)
{
@ -88,9 +122,7 @@ public class ProgressRing : RangeBase
}
}
private void UpdatePseudoClasses(
bool? isIndeterminate,
bool? preserveAspect)
private void UpdatePseudoClasses(bool? isIndeterminate, bool? preserveAspect)
{
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)
{
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!;
}
private static void OnEndAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e)
if (sender.fillArc is { } arc)
{
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;
@ -7,4 +8,9 @@ public static class StringFormatConverters
private static StringFormatValueConverter? _decimalConverter;
public static StringFormatValueConverter Decimal =>
_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.SetTheme(registryOptions.LoadTheme(ThemeName.DarkPlus));
editor.Options.ShowBoxForControlCharacters = false;
}
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>
/// Looks up a localized string similar to Automatically scroll to end of console output.
/// </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>
/// Looks up a localized string similar to Confirm Delete.
/// </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>
/// Looks up a localized string similar to Height.
/// </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>
/// Looks up a localized string similar to Import with Metadata.
/// </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>
/// Looks up a localized string similar to Inner exception.
/// </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>
/// Looks up a localized string similar to Output Browser.
/// </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>
/// Looks up a localized string similar to Python Packages.
/// </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>
/// Looks up a localized string similar to Text to Image.
/// </summary>

33
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -825,4 +825,37 @@
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>Additional folders such as IPAdapters and TextualInversions (embeddings) can be enabled here</value>
</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>

55
StabilityMatrix.Avalonia/Program.cs

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

46
StabilityMatrix.Avalonia/Styles/ProgressRing.axaml

@ -7,10 +7,13 @@
<Design.PreviewWith>
<StackPanel>
<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 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>
</StackPanel>
</Design.PreviewWith>
@ -22,27 +25,40 @@
<Style Selector="controls|ProgressRing">
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColor}"/>
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
<Setter Property="BorderThickness" Value="9" />
<Setter Property="StrokeThickness" Value="5" />
<Setter Property="MinHeight" 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">
<ControlTemplate>
<Panel x:Name="FluentRingRoot">
<Ellipse x:Name="Track"
<Panel x:Name="PART_RingRoot">
<Ellipse x:Name="PART_Track"
Stroke="{TemplateBinding Background}"
StrokeThickness="{Binding BorderThickness.Left, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
<Arc x:Name="Fill"
StrokeThickness="{TemplateBinding StrokeThickness}" />
<Arc x:Name="PART_Fill"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="{Binding BorderThickness.Left, RelativeSource={RelativeSource Mode=TemplatedParent}}"
StrokeThickness="{TemplateBinding StrokeThickness}"
StrokeLineCap="Round" />
</Panel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="controls|ProgressRing:not(:indeterminate) /template/ Arc#Fill">
<Setter Property="StartAngle" Value="{Binding StartAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
<Setter Property="SweepAngle" Value="{Binding ValueAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
<Style Selector="controls|ProgressRing:not(:indeterminate) /template/ Arc#PART_Fill">
<!--<Setter Property="StartAngle" Value="{Binding StartAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/>-->
<!--<Setter Property="SweepAngle" Value="{Binding SweepAngle, RelativeSource={RelativeSource Mode=TemplatedParent}}"/>-->
</Style>
<Style Selector="controls|ProgressRing:preserveaspect">
@ -50,14 +66,14 @@
<Setter Property="MinHeight" Value="32" />
</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="Height" Value="{Binding Width, RelativeSource={RelativeSource Mode=Self}}"/>
</Style>
<Style Selector="controls|ProgressRing[IsEnabled=True]:indeterminate /template/ Arc#Fill">
<Style Selector="controls|ProgressRing[IsEnabled=True]:indeterminate /template/ Arc#PART_Fill">
<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%">
<Setter Property="StartAngle" Value="-720"/>
<Setter Property="SweepAngle" Value="0"/>
@ -98,7 +114,7 @@
</Style.Animations>
</Style>
<Style Selector="controls|ProgressRing[IsEnabled=True] /template/ Ellipse#Track">
<Style Selector="controls|ProgressRing[IsEnabled=True]:not(:indeterminate) /template/ Ellipse#PART_Track">
<Style.Animations>
<Animation Duration="0:0:1" IterationCount="INFINITE">
<KeyFrame Cue="0%">

45
StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs

@ -54,8 +54,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
/// <summary>
/// Gets a cancellation token using the cursor lock timeout
/// </summary>
private CancellationToken WriteCursorLockTimeoutToken =>
new CancellationTokenSource(WriteCursorLockTimeout).Token;
private CancellationToken WriteCursorLockTimeoutToken => new CancellationTokenSource(WriteCursorLockTimeout).Token;
/// <summary>
/// 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))
{
Logger.ConditionalTrace(
$"Reset cursor to end: ({writeCursor} -> {Document.TextLength})"
);
Logger.ConditionalTrace($"Reset cursor to end: ({writeCursor} -> {Document.TextLength})");
writeCursor = Document.TextLength;
}
DebugPrintDocument();
@ -146,9 +143,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
Dispatcher.UIThread.VerifyAccess();
// Get cancellation token
var ct =
updateCts?.Token
?? throw new InvalidOperationException("Update cancellation token must be set");
var ct = updateCts?.Token ?? throw new InvalidOperationException("Update cancellation token must be set");
try
{
@ -176,9 +171,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
);
// Link the cancellation token to the write cursor lock timeout
var linkedCt = CancellationTokenSource
.CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken)
.Token;
var linkedCt = CancellationTokenSource.CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken).Token;
using (await writeCursorLock.LockAsync(linkedCt))
{
@ -193,10 +186,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
catch (Exception e)
{
// Log other errors and continue here to not crash the UI thread
Logger.Error(
e,
$"Unexpected error in console update loop: {e.GetType().Name} {e.Message}"
);
Logger.Error(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);
Logger.ConditionalTrace(
$"Moving cursor to start for carriage return "
+ $"({writeCursor} -> {lineStartOffset})"
$"Moving cursor to start for carriage return " + $"({writeCursor} -> {lineStartOffset})"
);
writeCursor = lineStartOffset;
}
@ -275,10 +264,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
else
{
// We want to move up one line
var targetLocation = new TextLocation(
currentLocation.Line - 1,
currentLocation.Column
);
var targetLocation = new TextLocation(currentLocation.Line - 1, currentLocation.Column);
var targetOffset = Document.GetOffset(targetLocation);
// 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
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
var spaces = new string(' ', currentLine.Length);
// Insert the text
Logger.ConditionalTrace(
$"Erasing line {currentLine.LineNumber}: (length = {currentLine.Length})"
);
Logger.ConditionalTrace($"Erasing line {currentLine.LineNumber}: (length = {currentLine.Length})");
using (Document.RunUpdate())
{
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);
Logger.ConditionalTrace(
$"Moving cursor to start of next line "
+ $"({writeCursor} -> {nextLine.Offset})"
$"Moving cursor to start of next line " + $"({writeCursor} -> {nextLine.Offset})"
);
writeCursor = nextLine.Offset;
}
@ -393,9 +382,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDis
if (remainingLength > 0)
{
var textToInsert = text[replaceLength..];
Logger.ConditionalTrace(
$"Inserting: (cursor = {writeCursor}, " + $"text = {textToInsert.ToRepr()})"
);
Logger.ConditionalTrace($"Inserting: (cursor = {writeCursor}, " + $"text = {textToInsert.ToRepr()})");
Document.Insert(writeCursor, textToInsert);
writeCursor += textToInsert.Length;

8
StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs

@ -7,6 +7,7 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.HardwareInfo;
namespace StabilityMatrix.Avalonia.ViewModels;
@ -25,8 +26,7 @@ public partial class FirstLaunchSetupViewModel : ViewModelBase
private RefreshBadgeViewModel checkHardwareBadge =
new()
{
WorkingToolTipText =
"We're checking some hardware specifications to determine compatibility.",
WorkingToolTipText = "We're checking some hardware specifications to determine compatibility.",
SuccessToolTipText = "Everything looks good!",
FailToolTipText =
"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());
}
// First Nvidia GPU
var activeGpu = gpuInfo.FirstOrDefault(
gpu => gpu.Name?.ToLowerInvariant().Contains("nvidia") ?? false
);
var activeGpu = gpuInfo.FirstOrDefault(gpu => gpu.Name?.ToLowerInvariant().Contains("nvidia") ?? false);
var isNvidia = activeGpu is not null;
// Otherwise first GPU
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.Views.Settings;
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 SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -9,12 +30,191 @@ namespace StabilityMatrix.Avalonia.ViewModels.Settings;
[View(typeof(InferenceSettingsPage))]
[Singleton, ManagedService]
public class InferenceSettingsViewModel : PageViewModelBase
public partial class InferenceSettingsViewModel : PageViewModelBase
{
private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager;
private readonly ICompletionProvider completionProvider;
/// <inheritdoc />
public override string Title => "Inference";
/// <inheritdoc />
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
public override IconSource IconSource => 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.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Python;
@ -73,8 +74,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
public SharedState SharedState { get; }
public override string Title => "Settings";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
// ReSharper disable once MemberCanBeMadeStatic.Global
public string AppVersion =>
@ -102,54 +102,35 @@ public partial class MainSettingsViewModel : PageViewModelBase
[ObservableProperty]
private bool removeSymlinksOnShutdown;
// Inference UI section
[ObservableProperty]
private bool isPromptCompletionEnabled = true;
// Integrations section
[ObservableProperty]
private IReadOnlyList<string> availableTagCompletionCsvs = Array.Empty<string>();
private bool isDiscordRichPresenceEnabled;
// Debug section
[ObservableProperty]
private string? selectedTagCompletionCsv;
private string? debugPaths;
[ObservableProperty]
private bool isCompletionRemoveUnderscoresEnabled = true;
private string? debugCompatInfo;
[ObservableProperty]
[CustomValidation(typeof(MainSettingsViewModel), nameof(ValidateOutputImageFileNameFormat))]
private string? outputImageFileNameFormat;
private string? debugGpuInfo;
[ObservableProperty]
private string? outputImageFileNameFormatSample;
#region System Info
public IEnumerable<FileNameFormatVar> OutputImageFileNameFormatVars =>
FileNameFormatProvider
.GetSample()
.Substitutions.Select(
kv =>
new FileNameFormatVar
{
Variable = $"{{{kv.Key}}}",
Example = kv.Value.Invoke()
}
);
private static Lazy<IReadOnlyList<GpuInfo>> GpuInfosLazy { get; } =
new(() => HardwareHelper.IterGpuInfo().ToImmutableArray());
[ObservableProperty]
private bool isImageViewerPixelGridEnabled = true;
public static IReadOnlyList<GpuInfo> GpuInfos => GpuInfosLazy.Value;
// Integrations section
[ObservableProperty]
private bool isDiscordRichPresenceEnabled;
private MemoryInfo memoryInfo;
// Debug section
[ObservableProperty]
private string? debugPaths;
private readonly DispatcherTimer hardwareInfoUpdateTimer = new() { Interval = TimeSpan.FromSeconds(2.627) };
[ObservableProperty]
private string? debugCompatInfo;
public Task<CpuInfo> CpuInfoAsync => HardwareHelper.GetCpuInfoAsync();
[ObservableProperty]
private string? debugGpuInfo;
#endregion
// Info section
private const int VersionTapCountThreshold = 7;
@ -162,8 +143,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
public string VersionFlyoutText =>
$"You are {VersionTapCountThreshold - VersionTapCount} clicks away from enabling Debug options.";
public string DataDirectory =>
settingsManager.IsLibraryDirSet ? settingsManager.LibraryDir : "Not set";
public string DataDirectory => settingsManager.IsLibraryDirSet ? settingsManager.LibraryDir : "Not set";
public MainSettingsViewModel(
INotificationService notificationService,
@ -211,77 +191,28 @@ public partial class MainSettingsViewModel : PageViewModelBase
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedAnimationScale,
settings => settings.AnimationScale
);
settingsManager.RelayPropertyFor(
this,
vm => vm.SelectedTagCompletionCsv,
settings => settings.TagCompletionCsv
);
settingsManager.RelayPropertyFor(this, vm => vm.SelectedAnimationScale, settings => settings.AnimationScale);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsPromptCompletionEnabled,
settings => settings.IsPromptCompletionEnabled,
true
);
DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsCompletionRemoveUnderscoresEnabled,
settings => settings.IsCompletionRemoveUnderscoresEnabled,
true
);
hardwareInfoUpdateTimer.Tick += OnHardwareInfoUpdateTimerTick;
}
this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat)
.Throttle(TimeSpan.FromMilliseconds(50))
.Subscribe(formatProperty =>
/// <inheritdoc />
public override void OnLoaded()
{
var provider = FileNameFormatProvider.GetSample();
var template = formatProperty.Value ?? string.Empty;
base.OnLoaded();
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";
hardwareInfoUpdateTimer.Start();
OnHardwareInfoUpdateTimerTick(null, null!);
}
});
settingsManager.RelayPropertyFor(
this,
vm => vm.OutputImageFileNameFormat,
settings => settings.InferenceOutputImageFileNameFormat,
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsImageViewerPixelGridEnabled,
settings => settings.IsImageViewerPixelGridEnabled,
true
);
/// <inheritdoc />
public override void OnUnloaded()
{
base.OnUnloaded();
DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(
notificationService,
LogLevel.Warn
);
ImportTagCsvCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
hardwareInfoUpdateTimer.Stop();
}
/// <inheritdoc />
@ -291,18 +222,13 @@ public partial class MainSettingsViewModel : PageViewModelBase
await notificationService.TryAsync(completionProvider.Setup());
UpdateAvailableTagCompletionCsvs();
// Start accounts update
accountsService.RefreshAsync().SafeFireAndForget();
}
public static ValidationResult ValidateOutputImageFileNameFormat(
string? format,
ValidationContext context
)
private void OnHardwareInfoUpdateTimerTick(object? sender, EventArgs e)
{
return FileNameFormatProvider.GetSample().Validate(format ?? string.Empty);
MemoryInfo = HardwareHelper.GetMemoryInfo();
}
partial void OnSelectedThemeChanged(string? value)
@ -341,7 +267,9 @@ public partial class MainSettingsViewModel : PageViewModelBase
CloseButtonText = Resources.Action_RelaunchLater
};
Dispatcher.UIThread.InvokeAsync(async () =>
Dispatcher
.UIThread
.InvokeAsync(async () =>
{
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
@ -352,11 +280,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
}
else
{
Logger.Info(
"Requested invalid language change from {Old} to {New}",
oldValue,
newValue
);
Logger.Info("Requested invalid language change from {Old} to {New}", oldValue, newValue);
}
}
@ -379,7 +303,9 @@ public partial class MainSettingsViewModel : PageViewModelBase
[RelayCommand]
private void NavigateToSubPage(Type viewModelType)
{
Dispatcher.UIThread.Post(
Dispatcher
.UIThread
.Post(
() =>
settingsNavigationService.NavigateTo(
viewModelType,
@ -397,8 +323,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
var viewModel = dialogFactory.Get<EnvVarsViewModel>();
// Load current settings
var current =
settingsManager.Settings.EnvironmentVariables ?? new Dictionary<string, string>();
var current = settingsManager.Settings.EnvironmentVariables ?? new Dictionary<string, string>();
viewModel.EnvVars = new ObservableCollection<EnvVarKeyPair>(
current.Select(kvp => new EnvVarKeyPair(kvp.Key, kvp.Value))
);
@ -414,7 +339,8 @@ public partial class MainSettingsViewModel : PageViewModelBase
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
// Save settings
var newEnvVars = viewModel.EnvVars
var newEnvVars = viewModel
.EnvVars
.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key))
.GroupBy(kvp => kvp.Key, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.First().Value, StringComparer.Ordinal);
@ -453,68 +379,6 @@ public partial class MainSettingsViewModel : PageViewModelBase
#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
/// <summary>
@ -531,10 +395,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
await using var _ = new MinimumDelay(200, 300);
var shortcutDir = new DirectoryPath(
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),
"Programs"
);
var shortcutDir = new DirectoryPath(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs");
var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk");
var appPath = Compat.AppCurrentPath;
@ -566,8 +427,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
// Confirmation dialog
var dialog = new BetterContentDialog
{
Title =
"This will create a shortcut for Stability Matrix in the Start Menu for all users",
Title = "This will create a shortcut for Stability Matrix in the Start Menu for all users",
Content = "You will be prompted for administrator privileges. Continue?",
PrimaryButtonText = Resources.Action_Yes,
CloseButtonText = Resources.Action_Cancel,
@ -764,16 +624,12 @@ public partial class MainSettingsViewModel : PageViewModelBase
private async Task DebugMakeImageGrid()
{
var provider = App.StorageProvider;
var files = await provider.OpenFilePickerAsync(
new FilePickerOpenOptions() { AllowMultiple = true }
);
var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions() { AllowMultiple = true });
if (files.Count == 0)
return;
var images = await files.SelectAsync(
async f => SKImage.FromEncodedData(await f.OpenReadAsync())
);
var images = await files.SelectAsync(async f => SKImage.FromEncodedData(await f.OpenReadAsync()));
var grid = ImageProcessor.CreateImageGrid(images.ToImmutableArray());
@ -923,11 +779,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
}
catch (Exception e)
{
notificationService.Show(
"Failed to read licenses information",
$"{e}",
NotificationType.Error
);
notificationService.Show("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 override string Title => "Settings";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
public IReadOnlyList<PageViewModelBase> SubPages { get; }
[ObservableProperty]
private ObservableCollection<PageViewModelBase> currentPagePath = new();
private ObservableCollection<PageViewModelBase> currentPagePath = [];
[ObservableProperty]
private PageViewModelBase? currentPage;

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

@ -34,11 +34,15 @@ public partial class PackageModificationDialog : UserControlBase
textMate.SetGrammar(scope);
textMate.SetTheme(options.LoadTheme(ThemeName.DarkPlus));
editor.Options.ShowBoxForControlCharacters = false;
}
EventManager.Instance.ScrollToBottomRequested += (_, _) =>
{
Dispatcher.UIThread.Invoke(() =>
Dispatcher
.UIThread
.Invoke(() =>
{
var editor = this.FindControl<TextEditor>("Console");
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:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
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:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vmSettings="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Settings"
d:DataContext="{x:Static mocks:DesignData.InferenceSettingsViewModel}"
d:DesignHeight="450"
d:DesignWidth="800"
d:DesignHeight="650"
d:DesignWidth="900"
x:DataType="vmSettings:InferenceSettingsViewModel"
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">
<StackPanel Margin="8,16" Spacing="8">
<!-- Theme -->
<Grid RowDefinitions="Auto,*,*,*">
<!-- Prompt -->
<sg:SpacedGrid RowDefinitions="Auto,*" RowSpacing="4">
<TextBlock
Margin="0,0,0,8"
Margin="0,0,0,4"
FontWeight="Medium"
Text="Stuff" />
Text="{x:Static lang:Resources.Label_Prompt}" />
<!-- Auto Completion -->
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Theme}"
IconSource="WeatherMoon">
<ui:SettingsExpander.Footer>
<ComboBox MinWidth="100" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0,8,4"
Header="{x:Static lang:Resources.Label_Language}"
IconSource="Character">
Margin="8,0"
Header="{x:Static lang:Resources.Label_AutoCompletion}">
<ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-solid fa-wand-magic-sparkles" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox MinWidth="100" />
<ToggleSwitch IsChecked="{Binding IsPromptCompletionEnabled}" />
</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
Grid.Row="3"
Margin="8,0,8,4"
Header="Inference"
IconSource="Code" />
</Grid>
</sg:SpacedGrid>
<!-- Checkpoints Manager Options -->
<Grid RowDefinitions="auto,*,Auto">
<!-- General -->
<sg:SpacedGrid RowDefinitions="Auto,*,*" RowSpacing="4">
<TextBlock
Margin="0,0,0,8"
Margin="0,0,0,4"
FontWeight="Medium"
Text="Other stuff" />
Text="{x:Static lang:Resources.Label_General}" />
<!-- Image Viewer -->
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0"
IconSource="Folder">
<ui:SettingsExpander.Footer>
<CheckBox Margin="8" />
</ui:SettingsExpander.Footer>
Header="{x:Static lang:Resources.Label_ImageViewer}"
IconSource="Image"
IsExpanded="True">
<!-- 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>
</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>
</ScrollViewer>
</controls:UserControlBase>

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

@ -7,6 +7,8 @@
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
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:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@ -28,10 +30,17 @@
<controls:UserControlBase.Resources>
<converters:CultureInfoDisplayConverter x:Key="CultureInfoDisplayConverter" />
<converters:IndexPlusOneConverter x:Key="IndexPlusOneConverter" />
<converters:EnumStringConverter x:Key="EnumStringConverter" />
<converters:EnumToBooleanConverter x:Key="EnumBoolConverter" />
</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">
<StackPanel Margin="8,16" Spacing="8">
<!-- Theme -->
@ -105,111 +114,29 @@
</ui:SettingsExpander>
</Grid>
<!-- Inference UI -->
<Grid Margin="0,8,0,0" RowDefinitions="auto,*,*,*">
<!-- General -->
<sg:SpacedGrid RowDefinitions="Auto,*" RowSpacing="4">
<TextBlock
Margin="0,0,0,8"
Margin="0,0,0,4"
FontWeight="Medium"
Text="Inference" />
<!-- Auto Completion -->
Text="{x:Static lang:Resources.Label_General}" />
<!-- Link to Inference Sub-Settings -->
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0,8,4"
Header="Prompt Auto Completion">
<ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-solid fa-wand-magic-sparkles" />
</ui:SettingsExpander.IconSource>
<!-- 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">
Margin="8,0"
ActionIconSource="ChevronRight"
Command="{Binding NavigateToSubPageCommand}"
CommandParameter="{x:Type vmSettings:InferenceSettingsViewModel}"
Header="{x:Static lang:Resources.Label_Inference}"
IsClickEnabled="True">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource Symbol="TabDesktopImage" />
<fluentIcons:SymbolIconSource
FontSize="10"
IsFilled="True"
Symbol="AppGeneric" />
</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: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>
</sg:SpacedGrid>
<!-- Environment Options -->
<Grid RowDefinitions="Auto, Auto, Auto">
@ -283,7 +210,7 @@
</sg:SpacedGrid>
<!-- System Options -->
<sg:SpacedGrid RowDefinitions="Auto,Auto,Auto,Auto" RowSpacing="4">
<sg:SpacedGrid RowDefinitions="Auto,Auto,Auto,Auto,Auto" RowSpacing="4">
<TextBlock
Margin="0,0,0,4"
FontWeight="Medium"
@ -292,7 +219,6 @@
<!-- Updates page -->
<ui:SettingsExpander
Grid.Row="1"
Margin="8,0"
ActionIconSource="ChevronRight"
Command="{Binding NavigateToSubPageCommand}"
CommandParameter="{x:Type vmSettings:UpdateSettingsViewModel}"
@ -308,7 +234,6 @@
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0"
Description="{x:Static lang:Resources.Label_AddToStartMenu_Details}"
Header="{x:Static lang:Resources.Label_AddToStartMenu}"
IconSource="StarAdd"
@ -348,7 +273,6 @@
<ui:SettingsExpander
Grid.Row="3"
Margin="8,0"
Description="{x:Static lang:Resources.Label_SelectNewDataDirectory_Details}"
IconSource="MoveToFolder">
<ui:SettingsExpander.Header>
@ -376,6 +300,121 @@
</Button>
</ui:SettingsExpander.Footer>
</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>
<!-- Debug Options -->

3
StabilityMatrix.Avalonia/Views/SettingsPage.axaml

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

46
StabilityMatrix.Core/Database/LiteDbContext.cs

@ -23,8 +23,7 @@ public class LiteDbContext : ILiteDbContext
public event EventHandler? CivitModelsChanged;
// Collections (Tables)
public ILiteCollectionAsync<CivitModel> CivitModels =>
Database.GetCollection<CivitModel>("CivitModels");
public ILiteCollectionAsync<CivitModel> CivitModels => Database.GetCollection<CivitModel>("CivitModels");
public ILiteCollectionAsync<CivitModelVersion> CivitModelVersions =>
Database.GetCollection<CivitModelVersion>("CivitModelVersions");
public ILiteCollectionAsync<CivitModelQueryCacheEntry> CivitModelQueryCache =>
@ -64,19 +63,12 @@ public class LiteDbContext : ILiteDbContext
{
var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db");
db = new LiteDatabaseAsync(
new ConnectionString()
{
Filename = dbPath,
Connection = ConnectionType.Shared,
}
new ConnectionString() { Filename = dbPath, Connection = ConnectionType.Shared, }
);
}
catch (IOException e)
{
logger.LogWarning(
"Database in use or not accessible ({Message}), using temporary database",
e.Message
);
logger.LogWarning("Database in use or not accessible ({Message}), using temporary database", e.Message);
}
}
@ -84,30 +76,18 @@ public class LiteDbContext : ILiteDbContext
db ??= new LiteDatabaseAsync(":temp:");
// Register reference fields
LiteDBExtensions.Register<CivitModel, CivitModelVersion>(
m => m.ModelVersions,
"CivitModelVersions"
);
LiteDBExtensions.Register<CivitModelQueryCacheEntry, CivitModel>(
e => e.Items,
"CivitModels"
);
LiteDBExtensions.Register<CivitModel, CivitModelVersion>(m => m.ModelVersions, "CivitModelVersions");
LiteDBExtensions.Register<CivitModelQueryCacheEntry, CivitModel>(e => e.Items, "CivitModels");
return db;
}
public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(
string hashBlake3
)
public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3)
{
var version = await CivitModelVersions
.Query()
.Where(
mv =>
mv.Files!
.Select(f => f.Hashes)
.Select(hashes => hashes.BLAKE3)
.Any(hash => hash == hashBlake3)
mv => mv.Files!.Select(f => f.Hashes).Select(hashes => hashes.BLAKE3).Any(hash => hash == hashBlake3)
)
.FirstOrDefaultAsync()
.ConfigureAwait(false);
@ -128,9 +108,7 @@ public class LiteDbContext : ILiteDbContext
public async Task<bool> UpsertCivitModelAsync(CivitModel civitModel)
{
// Insert model versions first then model
var versionsUpdated = await CivitModelVersions
.UpsertAsync(civitModel.ModelVersions)
.ConfigureAwait(false);
var versionsUpdated = await CivitModelVersions.UpsertAsync(civitModel.ModelVersions).ConfigureAwait(false);
var updated = await CivitModels.UpsertAsync(civitModel).ConfigureAwait(false);
// Notify listeners on any change
var anyUpdated = versionsUpdated > 0 || updated;
@ -182,8 +160,7 @@ public class LiteDbContext : ILiteDbContext
return null;
}
public Task<bool> UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) =>
GithubCache.UpsertAsync(cacheEntry);
public Task<bool> UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) => GithubCache.UpsertAsync(cacheEntry);
public void Dispose()
{
@ -194,6 +171,11 @@ public class LiteDbContext : ILiteDbContext
database.Dispose();
}
catch (ObjectDisposedException) { }
catch (ApplicationException)
{
// Ignores a mutex error from library
// https://stability-matrix.sentry.io/share/issue/5c62f37462444e7eab18cea314af231f/
}
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 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
{
< KiB => $"{bytes} B",
< MiB => $"{bytes / (double)KiB:0.0} KiB",
< GiB => $"{bytes / (double)MiB:0.0} MiB",
_ => $"{bytes / (double)GiB:0.0} GiB"
< KiB => $"{bytes:0} Bytes",
< MiB
=> (trimZero ? $"{bytes / (double)KiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)KiB:0.0}")
+ " 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
{
< KiB => $"{bytes} Bytes",
< MiB => $"{bytes / (double)KiB:0.0} KB",
< GiB => $"{bytes / (double)MiB:0.0} MB",
_ => $"{bytes / (double)GiB:0.00} GB"
< KiB => $"{bytes:0} Bytes",
< MiB
=> (trimZero ? $"{bytes / (double)KiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)KiB:0.0}")
+ " 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)
{
throw new NullReferenceException(
"GlobalConfig.LibraryDir was not set before being accessed.");
throw new NullReferenceException("GlobalConfig.LibraryDir was not set before being accessed.");
}
return libraryDir;
}
@ -31,11 +30,12 @@ public static class GlobalConfig
/// Full path to the %APPDATA% directory.
/// Usually C:\Users\{username}\AppData\Roaming
/// </summary>
public static DirectoryPath AppDataDir { get; } = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
public static DirectoryPath AppDataDir { get; } =
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
/// <summary>
/// Full path to the fixed home directory.
/// Currently %APPDATA%\StabilityMatrix
///</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 DateTimeOffset? LastUpdateCheck { get; set; }
public bool UpdateAvailable { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter<TorchVersion>))]
public TorchVersion? PreferredTorchVersion { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter<SharedFolderMethod>))]
public SharedFolderMethod? PreferredSharedFolderMethod { get; set; }
public bool UseSharedOutputFolder { get; set; }
/// <summary>
@ -188,10 +193,7 @@ public class InstalledPackage : IJsonOnDeserialized
var suffix = 2;
while (Directory.Exists(newPackagePath))
{
newPackagePath = System.IO.Path.Combine(
newPackagesDir,
$"{packageFolderName}-{suffix}"
);
newPackagePath = System.IO.Path.Combine(newPackagesDir, $"{packageFolderName}-{suffix}");
suffix++;
}
@ -234,16 +236,10 @@ public class InstalledPackage : IJsonOnDeserialized
if (Version != null)
return;
if (
string.IsNullOrWhiteSpace(InstalledBranch) && !string.IsNullOrWhiteSpace(PackageVersion)
)
if (string.IsNullOrWhiteSpace(InstalledBranch) && !string.IsNullOrWhiteSpace(PackageVersion))
{
// release mode
Version = new InstalledPackageVersion
{
InstalledReleaseVersion = PackageVersion,
IsPrerelease = false
};
Version = new InstalledPackageVersion { InstalledReleaseVersion = PackageVersion, IsPrerelease = false };
}
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
{
Bool,
String,
Int,
Int
}

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

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

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

@ -1,5 +1,6 @@
using Octokit;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
@ -80,29 +81,15 @@ public abstract class BasePackage
);
public virtual IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[]
{
SharedFolderMethod.Symlink,
SharedFolderMethod.Configuration,
SharedFolderMethod.None
};
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.Configuration, SharedFolderMethod.None };
public abstract SharedFolderMethod RecommendedSharedFolderMethod { get; }
public abstract Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
);
public abstract Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod);
public abstract Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
);
public abstract Task UpdateModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod);
public abstract Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
);
public abstract Task RemoveModelFolderLinks(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod);
public abstract Task SetupOutputFolderLinks(DirectoryPath installDirectory);
public abstract Task RemoveOutputFolderLinks(DirectoryPath installDirectory);
@ -125,10 +112,7 @@ public abstract class BasePackage
return TorchVersion.Rocm;
}
if (
HardwareHelper.PreferDirectML()
&& AvailableTorchVersions.Contains(TorchVersion.DirectMl)
)
if (HardwareHelper.PreferDirectML() && AvailableTorchVersions.Contains(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.
/// </summary>
public abstract Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public abstract Dictionary<
SharedOutputType,
IReadOnlyList<string>
>? SharedOutputFolders { get; }
public abstract Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders { get; }
public abstract Task<PackageVersionOptions> GetAllVersionOptions();
public abstract Task<IEnumerable<GitCommit>?> GetAllCommits(
string branch,
int page = 1,
int perPage = 10
);
public abstract Task<DownloadPackageVersionOptions> GetLatestVersion(
bool includePrerelease = false
);
public abstract Task<IEnumerable<GitCommit>?> GetAllCommits(string branch, int page = 1, int perPage = 10);
public abstract Task<DownloadPackageVersionOptions> GetLatestVersion(bool includePrerelease = false);
public abstract string MainBranch { get; }
public event EventHandler<int>? Exited;
public event EventHandler<string>? StartupComplete;
@ -178,9 +153,7 @@ public abstract class BasePackage
public void OnStartupComplete(string url) => StartupComplete?.Invoke(this, url);
public virtual PackageVersionType AvailableVersionTypes =>
ShouldIgnoreReleases
? PackageVersionType.Commit
: PackageVersionType.GithubRelease | PackageVersionType.Commit;
ShouldIgnoreReleases ? PackageVersionType.Commit : PackageVersionType.GithubRelease | PackageVersionType.Commit;
protected async Task InstallCudaTorch(
PyVenvRunner venvRunner,
@ -188,9 +161,7 @@ public abstract class BasePackage
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true)
);
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true));
await venvRunner
.PipInstall(
@ -210,9 +181,7 @@ public abstract class BasePackage
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for DirectML", isIndeterminate: true)
);
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for DirectML", isIndeterminate: true));
return venvRunner.PipInstall(new PipInstallArgs().WithTorchDirectML(), onConsoleOutput);
}
@ -223,13 +192,8 @@ public abstract class BasePackage
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for CPU", isIndeterminate: true)
);
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CPU", isIndeterminate: true));
return venvRunner.PipInstall(
new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision(),
onConsoleOutput
);
return venvRunner.PipInstall(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.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
@ -23,8 +24,7 @@ public class ComfyUI : BaseGitPackage
public override string DisplayName { get; set; } = "ComfyUI";
public override string Author => "comfyanonymous";
public override string LicenseType => "GPL-3.0";
public override string LicenseUrl =>
"https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE";
public override string LicenseUrl => "https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE";
public override string Blurb => "A powerful and modular stable diffusion GUI and backend";
public override string LaunchCommand => "main.py";
@ -35,8 +35,7 @@ public class ComfyUI : BaseGitPackage
public override string OutputFolderName => "output";
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced;
public override SharedFolderMethod RecommendedSharedFolderMethod =>
SharedFolderMethod.Configuration;
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration;
public ComfyUI(
IGithubApiCache githubApi,
@ -87,13 +86,10 @@ public class ComfyUI : BaseGitPackage
{
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
{
Level.Low => "--lowvram",
Level.Medium => "--normalvram",
MemoryLevel.Low => "--lowvram",
MemoryLevel.Medium => "--normalvram",
_ => null
},
Options = { "--highvram", "--normalvram", "--lowvram", "--novram" }
@ -103,12 +99,7 @@ public class ComfyUI : BaseGitPackage
Name = "Preview Method",
Type = LaunchOptionType.Bool,
InitialValue = "--preview-method auto",
Options =
{
"--preview-method auto",
"--preview-method latent2rgb",
"--preview-method taesd"
}
Options = { "--preview-method auto", "--preview-method latent2rgb", "--preview-method taesd" }
},
new()
{
@ -149,14 +140,7 @@ public class ComfyUI : BaseGitPackage
public override string MainBranch => "master";
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[]
{
TorchVersion.Cpu,
TorchVersion.Cuda,
TorchVersion.DirectMl,
TorchVersion.Rocm,
TorchVersion.Mps
};
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm, TorchVersion.Mps };
public override async Task InstallPackage(
string installLocation,
@ -217,9 +201,7 @@ public class ComfyUI : BaseGitPackage
}
// Install requirements file (skip torch)
progress?.Report(
new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true)
);
progress?.Report(new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true));
var requirementsFile = new FilePath(installLocation, "requirements.txt");
@ -227,15 +209,10 @@ public class ComfyUI : BaseGitPackage
.PipInstallFromRequirements(requirementsFile, onConsoleOutput, excludes: "torch")
.ConfigureAwait(false);
progress?.Report(
new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false)
);
progress?.Report(new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false));
}
private async Task AutoDetectAndInstallTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null
)
private async Task AutoDetectAndInstallTorch(PyVenvRunner venvRunner, IProgress<ProgressReport>? progress = null)
{
var gpus = HardwareHelper.IterGpuInfo().ToList();
if (gpus.Any(g => g.IsNvidia))
@ -292,10 +269,7 @@ public class ComfyUI : BaseGitPackage
VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
}
public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
public override Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod)
{
switch (sharedFolderMethod)
{
@ -330,9 +304,7 @@ public class ComfyUI : BaseGitPackage
throw new Exception("Invalid extra_model_paths.yaml");
}
// check if we have a child called "stability_matrix"
var stabilityMatrixNode = mappingNode.Children.FirstOrDefault(
c => c.Key.ToString() == "stability_matrix"
);
var stabilityMatrixNode = mappingNode.Children.FirstOrDefault(c => c.Key.ToString() == "stability_matrix");
if (stabilityMatrixNode.Key != null)
{
@ -363,10 +335,7 @@ public class ComfyUI : BaseGitPackage
{
{ "checkpoints", Path.Combine(modelsDir, "StableDiffusion") },
{ "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",
$"{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();
foreach (
var child in mappingNode.Children.Where(c => c.Key.ToString() != "stability_matrix")
)
foreach (var child in mappingNode.Children.Where(c => c.Key.ToString() != "stability_matrix"))
{
newRootNode.Children.Add(child);
}
newRootNode.Children.Add(stabilityMatrixNode);
var serializer = new SerializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.Build();
var serializer = new SerializerBuilder().WithNamingConvention(UnderscoredNamingConvention.Instance).Build();
var yamlData = serializer.Serialize(newRootNode);
File.WriteAllText(extraPathsYamlPath, yamlData);
return Task.CompletedTask;
}
public override Task UpdateModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
) =>
public override Task UpdateModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) =>
sharedFolderMethod switch
{
SharedFolderMethod.Symlink
=> base.UpdateModelFolders(installDirectory, sharedFolderMethod),
SharedFolderMethod.Configuration
=> SetupModelFolders(installDirectory, sharedFolderMethod),
SharedFolderMethod.Symlink => base.UpdateModelFolders(installDirectory, sharedFolderMethod),
SharedFolderMethod.Configuration => SetupModelFolders(installDirectory, sharedFolderMethod),
SharedFolderMethod.None => Task.CompletedTask,
_ => Task.CompletedTask
};
public override Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
)
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod)
{
return sharedFolderMethod switch
{
SharedFolderMethod.Configuration => RemoveConfigSection(installDirectory),
SharedFolderMethod.None => Task.CompletedTask,
SharedFolderMethod.Symlink
=> base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod),
SharedFolderMethod.Symlink => base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod),
_ => Task.CompletedTask
};
}
@ -457,9 +413,7 @@ public class ComfyUI : BaseGitPackage
mappingNode.Children.Remove("stability_matrix");
var serializer = new SerializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.Build();
var serializer = new SerializerBuilder().WithNamingConvention(UnderscoredNamingConvention.Instance).Build();
var yamlData = serializer.Serialize(mappingNode);
File.WriteAllText(extraPathsYamlPath, yamlData);
@ -472,18 +426,13 @@ public class ComfyUI : BaseGitPackage
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true)
);
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true));
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false);
await venvRunner
.PipInstall(
new PipInstallArgs()
.WithTorch("==2.0.1")
.WithTorchVision()
.WithTorchExtraIndex("rocm5.6"),
new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision().WithTorchExtraIndex("rocm5.6"),
onConsoleOutput
)
.ConfigureAwait(false);

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

@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
@ -26,17 +27,14 @@ public class Fooocus : BaseGitPackage
public override string DisplayName { get; set; } = "Fooocus";
public override string Author => "lllyasviel";
public override string Blurb =>
"Fooocus is a rethinking of Stable Diffusion and Midjourney’s designs";
public override string Blurb => "Fooocus is a rethinking of Stable Diffusion and Midjourney’s designs";
public override string LicenseType => "GPL-3.0";
public override string LicenseUrl => "https://github.com/lllyasviel/Fooocus/blob/main/LICENSE";
public override string LaunchCommand => "launch.py";
public override Uri PreviewImageUri =>
new(
"https://user-images.githubusercontent.com/19834515/261830306-f79c5981-cf80-4ee3-b06b-3fef3f8bfbc7.png"
);
new("https://user-images.githubusercontent.com/19834515/261830306-f79c5981-cf80-4ee3-b06b-3fef3f8bfbc7.png");
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
@ -79,13 +77,10 @@ public class Fooocus : BaseGitPackage
{
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
{
Level.Low => "--lowvram",
Level.Medium => "--normalvram",
MemoryLevel.Low => "--lowvram",
MemoryLevel.Medium => "--normalvram",
_ => null
},
Options = { "--highvram", "--normalvram", "--lowvram", "--novram" }
@ -158,8 +153,7 @@ public class Fooocus : BaseGitPackage
Action<ProcessOutput>? onConsoleOutput = null
)
{
var venvRunner = await SetupVenv(installLocation, forceRecreate: true)
.ConfigureAwait(false);
var venvRunner = await SetupVenv(installLocation, forceRecreate: true).ConfigureAwait(false);
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.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
@ -32,11 +33,9 @@ public class KohyaSs : BaseGitPackage
public override string Name => "kohya_ss";
public override string DisplayName { get; set; } = "kohya_ss";
public override string Author => "bmaltais";
public override string Blurb =>
"A Windows-focused Gradio GUI for Kohya's Stable Diffusion trainers";
public override string Blurb => "A Windows-focused Gradio GUI for Kohya's Stable Diffusion trainers";
public override string LicenseType => "Apache-2.0";
public override string LicenseUrl =>
"https://github.com/bmaltais/kohya_ss/blob/master/LICENSE.md";
public override string LicenseUrl => "https://github.com/bmaltais/kohya_ss/blob/master/LICENSE.md";
public override string LaunchCommand => "kohya_gui.py";
public override Uri PreviewImageUri =>
@ -49,16 +48,14 @@ public class KohyaSs : BaseGitPackage
public override TorchVersion GetRecommendedTorchVersion() => TorchVersion.Cuda;
public override string Disclaimer =>
"Nvidia GPU with at least 8GB VRAM is recommended. May be unstable on Linux.";
public override string Disclaimer => "Nvidia GPU with at least 8GB VRAM is recommended. May be unstable on Linux.";
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.UltraNightmare;
public override bool OfferInOneClickInstaller => false;
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.None;
public override IEnumerable<TorchVersion> AvailableTorchVersions => new[] { TorchVersion.Cuda };
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.None };
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods => new[] { SharedFolderMethod.None };
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
@ -126,9 +123,7 @@ public class KohyaSs : BaseGitPackage
{
if (Compat.IsWindows)
{
progress?.Report(
new ProgressReport(-1f, "Installing prerequisites...", isIndeterminate: true)
);
progress?.Report(new ProgressReport(-1f, "Installing prerequisites...", isIndeterminate: true));
await PrerequisiteHelper.InstallTkinterIfNecessary(progress).ConfigureAwait(false);
}
@ -203,9 +198,7 @@ public class KohyaSs : BaseGitPackage
"""
);
var replacementAcceleratePath = Compat.IsWindows
? @".\venv\scripts\accelerate"
: "./venv/bin/accelerate";
var replacementAcceleratePath = Compat.IsWindows ? @".\venv\scripts\accelerate" : "./venv/bin/accelerate";
var replacer = scope.InvokeMethod(
"StringReplacer",
@ -259,10 +252,7 @@ public class KohyaSs : BaseGitPackage
}
public override Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public override Dictionary<
SharedOutputType,
IReadOnlyList<string>
>? SharedOutputFolders { get; }
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders { get; }
public override string MainBranch => "master";
@ -278,13 +268,7 @@ public class KohyaSs : BaseGitPackage
if (!Compat.IsWindows)
return env;
var tkPath = Path.Combine(
SettingsManager.LibraryDir,
"Assets",
"Python310",
"tcl",
"tcl8.6"
);
var tkPath = Path.Combine(SettingsManager.LibraryDir, "Assets", "Python310", "tcl", "tcl8.6");
env["TCL_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.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
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 Author => "anapnoe";
public override string LicenseType => "AGPL-3.0";
public override string LicenseUrl =>
"https://github.com/anapnoe/stable-diffusion-webui-ux/blob/master/LICENSE.txt";
public override string LicenseUrl => "https://github.com/anapnoe/stable-diffusion-webui-ux/blob/master/LICENSE.txt";
public override string Blurb =>
"A pixel perfect design, mobile friendly, customizable interface that adds accessibility, "
+ "ease of use and extended functionallity to the stable diffusion web ui.";
public override string LaunchCommand => "launch.py";
public override Uri PreviewImageUri =>
new(
"https://raw.githubusercontent.com/anapnoe/stable-diffusion-webui-ux/master/screenshot.png"
);
new("https://raw.githubusercontent.com/anapnoe/stable-diffusion-webui-ux/master/screenshot.png");
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
@ -99,13 +97,10 @@ public class StableDiffusionUx : BaseGitPackage
{
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch
{
Level.Low => "--lowvram",
Level.Medium => "--medvram",
MemoryLevel.Low => "--lowvram",
MemoryLevel.Medium => "--medvram",
_ => null
},
Options = new() { "--lowvram", "--medvram", "--medvram-sdxl" }
@ -210,9 +205,7 @@ public class StableDiffusionUx : BaseGitPackage
}
// Install requirements file
progress?.Report(
new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true)
);
progress?.Report(new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true));
Logger.Info("Installing requirements_versions.txt");
var requirements = new FilePath(installLocation, "requirements_versions.txt");
@ -259,18 +252,13 @@ public class StableDiffusionUx : BaseGitPackage
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true)
);
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true));
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false);
await venvRunner
.PipInstall(
new PipInstallArgs()
.WithTorch("==2.0.1")
.WithTorchVision()
.WithTorchExtraIndex("rocm5.1.1"),
new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision().WithTorchExtraIndex("rocm5.1.1"),
onConsoleOutput
)
.ConfigureAwait(false);

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

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

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

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

2
StabilityMatrix.Core/StabilityMatrix.Core.csproj

@ -7,6 +7,7 @@
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
@ -25,6 +26,7 @@
<PackageReference Include="DeviceId.Windows" Version="6.5.0" />
<PackageReference Include="DeviceId.Windows.Wmi" Version="6.5.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="LiteDB" Version="5.0.17" />
<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 StabilityMatrix.Core.Converters.Json;
@ -46,6 +47,16 @@ public class DefaultUnknownEnumConverterTests
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]
public void TestSerialize_DefaultEnum_ShouldConvert()
{
@ -56,6 +67,26 @@ public class DefaultUnknownEnumConverterTests
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
{
Unknown,
@ -68,6 +99,8 @@ public class DefaultUnknownEnumConverterTests
{
Unknown,
Value1,
[EnumMember(Value = "Value 2")]
Value2
}

Loading…
Cancel
Save