Compare commits
337 Commits
dependabot
...
main
Author | SHA1 | Date |
---|---|---|
JT | 3d9c193e02 | 6 months ago |
JT | ad79d6e13a | 6 months ago |
JT | 72917d0163 | 6 months ago |
JT | 72a3b9163b | 6 months ago |
JT | 9f1c3d5623 | 6 months ago |
JT | 4b86f28bb8 | 6 months ago |
JT | bf749904a5 | 6 months ago |
Ionite | b199f5e5fd | 6 months ago |
Ionite | 8a202c7aef | 6 months ago |
Ionite | 93a16cbd3e | 6 months ago |
JT | d7f977fb86 | 7 months ago |
let | 11fdbdfeed | 7 months ago |
JT | d544351dd2 | 7 months ago |
JT | 0ede21d512 | 7 months ago |
JT | 5c838e9f98 | 7 months ago |
JT | c7b638182f | 7 months ago |
JT | 7cea793c18 | 7 months ago |
JT | 9e530a00b6 | 7 months ago |
JT | 9b13cb5a76 | 7 months ago |
JT | 4524b9c15b | 7 months ago |
JT | aca2e7a767 | 7 months ago |
JT | 52597ad675 | 7 months ago |
JT | 9fd20a7dd6 | 7 months ago |
JT | e1a9202cf7 | 7 months ago |
JT | 5eaceaac37 | 7 months ago |
JT | ae82e598a6 | 7 months ago |
JT | 56f5dca3bb | 7 months ago |
JT | 7e436ba132 | 7 months ago |
JT | f06b9ef077 | 7 months ago |
JT | 919a013f1b | 7 months ago |
JT | 81cf470e2d | 7 months ago |
JT | 14b3bd191e | 7 months ago |
JT | 9ae41313aa | 7 months ago |
JT | 047d973b11 | 7 months ago |
jt | d543b1a36e | 7 months ago |
JT | e3df430df5 | 7 months ago |
JT | 1e1cb10940 | 7 months ago |
JT | c66c4972b8 | 7 months ago |
JT | 7b962dbae5 | 7 months ago |
JT | 2de9fdcdf6 | 7 months ago |
JT | 153f89ed87 | 7 months ago |
JT | db777eeb3c | 7 months ago |
JT | c18e93271e | 7 months ago |
JT | 61c229da44 | 7 months ago |
JT | 10d468f455 | 7 months ago |
JT | 72394aa1ae | 7 months ago |
JT | 805d3f61e6 | 7 months ago |
JT | 3b8b8cc7d5 | 7 months ago |
JT | e626c47305 | 7 months ago |
JT | d73ca680ab | 7 months ago |
JT | fa458e2866 | 7 months ago |
JT | 041ec59914 | 7 months ago |
JT | c285a0c110 | 7 months ago |
JT | a8b4d680a1 | 7 months ago |
JT | f86576c9d8 | 7 months ago |
JT | aa032e01b9 | 7 months ago |
JT | dbd9efb311 | 7 months ago |
JT | 805dde5bd5 | 7 months ago |
Ionite | 9843b6f7ae | 7 months ago |
JT | d441ea94ae | 7 months ago |
JT | f97cabc755 | 7 months ago |
JT | a66063b031 | 7 months ago |
JT | 921a54012e | 7 months ago |
JT | 42545f03f3 | 7 months ago |
JT | e28a3980bf | 7 months ago |
JT | f2dde2a50e | 7 months ago |
JT | 708743df47 | 7 months ago |
jt | 848468d525 | 7 months ago |
JT | 662022e191 | 7 months ago |
JT | d46b65f3d3 | 7 months ago |
JT | 68a9968794 | 7 months ago |
Ionite | a652287baf | 7 months ago |
Ionite | 64c55ec379 | 7 months ago |
Ionite | 128ef13229 | 7 months ago |
Ionite | 0273d8dd3c | 7 months ago |
Ionite | 68647f391e | 7 months ago |
Ionite | 01cc2f89a7 | 7 months ago |
Ionite | a75908e4ef | 7 months ago |
Ionite | d3b37229da | 7 months ago |
Ionite | 90e815a421 | 7 months ago |
JT | fa1c0a403f | 7 months ago |
JT | ef25d5b944 | 7 months ago |
Ionite | e41b4195dc | 7 months ago |
Ionite | 49aa554257 | 7 months ago |
JT | b95bf2cabd | 7 months ago |
JT | c029cc9660 | 7 months ago |
JT | 9ac2b91b21 | 7 months ago |
JT | 5d42c7f9d6 | 7 months ago |
JT | 56acf60ead | 7 months ago |
JT | 2f89eeba4d | 7 months ago |
Ionite | c2efe33c91 | 7 months ago |
Ionite | 2d961be0f3 | 7 months ago |
Ionite | b21e11c0c8 | 7 months ago |
Ionite | 224b008cd9 | 7 months ago |
Ionite | 38a104146f | 7 months ago |
Ionite | 94ff2fa40b | 7 months ago |
Ionite | 04e5459f75 | 7 months ago |
Ionite | 8e1c00312c | 7 months ago |
JT | 2a22a43cc8 | 7 months ago |
JT | 92ddc09d07 | 7 months ago |
JT | 795413780c | 7 months ago |
JT | 8ba15c73a1 | 7 months ago |
JT | 96f507619c | 7 months ago |
JT | 07af1c85d5 | 7 months ago |
JT | 7bf3a7b4ac | 7 months ago |
JT | d211db88ab | 7 months ago |
Ionite | 9716ae2bf2 | 7 months ago |
Ionite | 43f3c28b9a | 7 months ago |
Ionite | 4e42eee757 | 7 months ago |
JT | 60ab7bb3a7 | 7 months ago |
JT | 5441572e38 | 7 months ago |
JT | 612509f4ff | 7 months ago |
JT | c8ab3a743e | 7 months ago |
JT | 9b0e1b90f5 | 7 months ago |
JT | e5d215463e | 7 months ago |
JT | 3d1090573b | 7 months ago |
JT | 026b9d0cb1 | 8 months ago |
JT | 9c3e80cb4f | 8 months ago |
JT | 9b193f9cb5 | 8 months ago |
JT | 2148628aaa | 8 months ago |
JT | 773a3a0c1c | 8 months ago |
JT | 9937b1ec6e | 8 months ago |
Ionite | c6a8722ab2 | 8 months ago |
Ionite | df3a602e3a | 8 months ago |
Ionite | 2d95adbda7 | 8 months ago |
JT | 5d43a8df18 | 8 months ago |
Ionite | 6bb8cec4b8 | 8 months ago |
Ionite | 660dad4245 | 8 months ago |
JT | 77a60b1a59 | 8 months ago |
JT | 727b04ca01 | 8 months ago |
Ionite | 6621a8ce58 | 8 months ago |
Ionite | ff26180e8b | 8 months ago |
JT | f224c77349 | 8 months ago |
JT | 67ce55d429 | 8 months ago |
Ionite | ae2c9b2783 | 8 months ago |
Ionite | 395d9c3963 | 8 months ago |
Ionite | 3161a009c7 | 8 months ago |
Ionite | d6920be164 | 8 months ago |
Ionite | 255b1feb66 | 8 months ago |
Ionite | 52b32f9227 | 8 months ago |
Ionite | 51b6e1be36 | 8 months ago |
Ionite | cefb08e4da | 8 months ago |
Ionite | 92d42b3c53 | 8 months ago |
Ionite | af89e07c01 | 8 months ago |
Ionite | d37a2df700 | 8 months ago |
Ionite | 4058742cd9 | 8 months ago |
JT | 5d6638334a | 8 months ago |
JT | 9d99c59747 | 8 months ago |
JT | 61abe7a1f6 | 8 months ago |
JT | 04db64fcfb | 8 months ago |
JT | 5419e7a53b | 8 months ago |
JT | ca67f73ff6 | 8 months ago |
JT | c7c5653ff3 | 8 months ago |
JT | 26c9db8b14 | 8 months ago |
JT | 772d93d46d | 8 months ago |
JT | 870aa20a80 | 8 months ago |
JT | 72f4cf04a7 | 8 months ago |
JT | 0409e0354d | 8 months ago |
JT | 864391db97 | 8 months ago |
JT | 597c4c9aec | 8 months ago |
JT | 035d54c844 | 8 months ago |
JT | 73771a29c1 | 8 months ago |
JT | 3af08e04f3 | 8 months ago |
JT | b2a76056af | 8 months ago |
JT | f40f0b6cc0 | 8 months ago |
JT | 7b01968b00 | 8 months ago |
JT | f30181caa8 | 8 months ago |
JT | c217189ade | 8 months ago |
JT | 8d59e1d9f2 | 8 months ago |
JT | d2ceefdd19 | 8 months ago |
JT | 4fe73777e6 | 8 months ago |
JT | d36f7c8c4a | 8 months ago |
Ionite | 565cfa918b | 8 months ago |
JT | c8a2afec28 | 8 months ago |
JT | 2b5db18871 | 8 months ago |
JT | acca9e685b | 8 months ago |
JT | 29b6863943 | 8 months ago |
JT | b913e45d5c | 8 months ago |
JT | 9ca69298d5 | 8 months ago |
JT | d31ae40b2d | 8 months ago |
JT | fcca600fba | 8 months ago |
JT | 233eba2fce | 8 months ago |
JT | 4393c01899 | 8 months ago |
JT | cbc37fb466 | 8 months ago |
JT | 0eb88e2255 | 8 months ago |
JT | 4846d69043 | 8 months ago |
JT | 19cd60c195 | 8 months ago |
JT | 078a471eaa | 8 months ago |
JT | db239e7c2e | 8 months ago |
JT | 6657cf59ea | 8 months ago |
JT | 8810e18ef6 | 8 months ago |
JT | 76cf8fbd41 | 8 months ago |
JT | 50b142ba54 | 8 months ago |
JT | 38013c16d6 | 8 months ago |
JT | d01cc2aeb4 | 8 months ago |
JT | ec4c7343da | 8 months ago |
JT | 86bcdaf86f | 8 months ago |
JT | a5beaf434c | 8 months ago |
Ionite | 1779da0210 | 8 months ago |
Ionite | 479aceebb0 | 8 months ago |
Ionite | b74ee76130 | 8 months ago |
JT | 662c575fe8 | 8 months ago |
Ionite | c119a3be91 | 8 months ago |
JT | 87828cb172 | 8 months ago |
JT | 9c78ef2197 | 8 months ago |
Ionite | 8503e937ed | 8 months ago |
Ionite | c028a51d71 | 8 months ago |
JT | 243115b459 | 8 months ago |
JT | 2f0e081e90 | 8 months ago |
JT | 977219995d | 8 months ago |
JT | 3c23d6774e | 8 months ago |
JT | b28aef1e7b | 8 months ago |
JT | 8cebc775e8 | 8 months ago |
JT | a58f4a9cc0 | 8 months ago |
JT | 393943abad | 8 months ago |
JT | 79698b8b1f | 8 months ago |
JT | d7086051d8 | 8 months ago |
JT | f2051dfeb1 | 8 months ago |
JT | e4aa0f85a5 | 8 months ago |
JT | 4d42cab3f4 | 8 months ago |
JT | 94cb39ce2b | 8 months ago |
JT | dada768a75 | 8 months ago |
JT | 67dd8c53b6 | 8 months ago |
JT | e0f04bc87e | 8 months ago |
JT | 8fdd737c51 | 8 months ago |
JT | 88504e0304 | 8 months ago |
JT | 99c8c926f2 | 8 months ago |
Ionite | b418a38ba5 | 8 months ago |
Ionite | bd28f00d8b | 8 months ago |
Ionite | 807a20c25b | 8 months ago |
Ionite | 8c2ea813db | 8 months ago |
Ionite | ec46a9c275 | 8 months ago |
Ionite | 23a4ec9fd2 | 8 months ago |
JT | c1d697bfcd | 8 months ago |
JT | 0e6cf4db7e | 8 months ago |
Ionite | 4be0ea4c8c | 8 months ago |
JT | 17560f40fe | 8 months ago |
JT | 02c64e2df1 | 8 months ago |
JT | 7c2f51cc09 | 8 months ago |
Ionite | 1270c13ede | 8 months ago |
Ionite | 80f9bf3a22 | 8 months ago |
Ionite | 2695f94b8b | 8 months ago |
Ionite | f378b8dac8 | 8 months ago |
Ionite | f781fbb229 | 8 months ago |
Ionite | 1dfd2eb4e5 | 8 months ago |
Ionite | d54e28acb4 | 8 months ago |
Ionite | 810b22cd08 | 8 months ago |
Ionite | 3a3f94621b | 8 months ago |
Ionite | 0738c491af | 8 months ago |
Ionite | adaff0357e | 8 months ago |
Ionite | 425b343480 | 8 months ago |
Ionite | 4bfa567ed4 | 8 months ago |
Ionite | 30609dc486 | 8 months ago |
Ionite | 664c8e5ba5 | 8 months ago |
Ionite | e7be967cdd | 8 months ago |
Ionite | 8b0bd6acbc | 8 months ago |
JT | 86d33c952f | 8 months ago |
JT | a6cb9751ac | 8 months ago |
Ionite | cccedd3b9b | 8 months ago |
Ionite | 030a25594c | 8 months ago |
Ionite | eee89bba1a | 8 months ago |
JT | c6ed1ba879 | 8 months ago |
JT | c885520193 | 8 months ago |
JT | 8edafa642c | 8 months ago |
JT | 78ec821ff9 | 8 months ago |
JT | d8ad305391 | 8 months ago |
JT | e93a097f71 | 8 months ago |
Ionite | 93b820c7e4 | 8 months ago |
Ionite | 1fa934a5fa | 8 months ago |
Ionite | b805a9e91c | 8 months ago |
Ionite | f9e305b803 | 8 months ago |
Ionite | 6055b7454d | 8 months ago |
Ionite | 75be254457 | 9 months ago |
Ionite | 9de2bfe668 | 9 months ago |
JT | 6fb5b0cb17 | 9 months ago |
JT | c2b3337fe3 | 9 months ago |
JT | 9facb59287 | 9 months ago |
JT | 8bbeae6b5e | 9 months ago |
JT | aed6e79957 | 9 months ago |
JT | b394e5b701 | 9 months ago |
JT | 987a224681 | 9 months ago |
JT | 09c72677b7 | 9 months ago |
Ionite | d5f726d9d6 | 9 months ago |
JT | 8c2e38e8ec | 9 months ago |
JT | 7a4a403bdb | 9 months ago |
JT | ea9dc5a3d2 | 9 months ago |
JT | 2a574c2cd8 | 9 months ago |
JT | 9abbaa0f16 | 9 months ago |
Ionite | a15dd34d97 | 9 months ago |
JT | 29dd89550d | 9 months ago |
Ionite | e94760b5ce | 9 months ago |
Ionite | c2e62926f3 | 9 months ago |
JT | 685662302d | 9 months ago |
JT | 494f5f9e32 | 9 months ago |
JT | 8f53c281b3 | 9 months ago |
Ionite | 842ab35e7d | 9 months ago |
JT | 11ddd89b09 | 9 months ago |
Ionite | 96c30f5d40 | 9 months ago |
Ionite | d329f37e9d | 9 months ago |
JT | 24b34f76d6 | 9 months ago |
JT | 2303759eec | 9 months ago |
Ionite | ecc3b7da2d | 9 months ago |
Ionite | 3653fb475e | 9 months ago |
Ionite | deb86fef83 | 9 months ago |
Ionite | cd0957825f | 9 months ago |
Ionite | 4cccde8a4b | 9 months ago |
Ionite | a0221c7d91 | 9 months ago |
Ionite | e1816f71c6 | 9 months ago |
Ionite | dc4e99b489 | 9 months ago |
JT | 7e7b0148aa | 9 months ago |
JT | 91fb39f366 | 9 months ago |
JT | 45f48ce36f | 9 months ago |
Ionite | 1ea83f30f9 | 9 months ago |
Ionite | e62546691a | 9 months ago |
Ionite | 0c30b4ba32 | 9 months ago |
Ionite | df1f188fe6 | 9 months ago |
Ionite | 92620dc894 | 9 months ago |
Ionite | eafb905929 | 9 months ago |
JT | f7c99cdba9 | 9 months ago |
JT | eb78ec5010 | 9 months ago |
JT | fb8f06d611 | 9 months ago |
JT | d4354d0d20 | 9 months ago |
JT | 49273b049a | 9 months ago |
JT | a1735e5837 | 9 months ago |
JT | fbc8111952 | 9 months ago |
JT | b5529f6ece | 9 months ago |
jt | c7d6f66909 | 9 months ago |
JT | 7e3e520b75 | 9 months ago |
JT | be8d2cd78d | 9 months ago |
JT | 5e7b9286ac | 9 months ago |
JT | 57a0597296 | 9 months ago |
JT | 889877724b | 9 months ago |
JT | 9f430ecb65 | 9 months ago |
JT | e87b5032be | 9 months ago |
JT | c26960ce1f | 9 months ago |
JT | b4af5906fd | 9 months ago |
JT | 2998ceee21 | 9 months ago |
214 changed files with 18018 additions and 1724 deletions
@ -0,0 +1,5 @@ |
|||||||
|
<Project> |
||||||
|
<PropertyGroup> |
||||||
|
<AvaloniaVersion>11.0.9</AvaloniaVersion> |
||||||
|
</PropertyGroup> |
||||||
|
</Project> |
@ -0,0 +1,208 @@ |
|||||||
|
using System; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using Avalonia; |
||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.Input; |
||||||
|
using Avalonia.Interactivity; |
||||||
|
using Avalonia.Xaml.Interactions.DragAndDrop; |
||||||
|
using Avalonia.Xaml.Interactivity; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls; |
||||||
|
|
||||||
|
public class BetterContextDragBehavior : Behavior<Control> |
||||||
|
{ |
||||||
|
private Point _dragStartPoint; |
||||||
|
private PointerEventArgs? _triggerEvent; |
||||||
|
private bool _lock; |
||||||
|
private bool _captured; |
||||||
|
|
||||||
|
public static readonly StyledProperty<object?> ContextProperty = AvaloniaProperty.Register< |
||||||
|
ContextDragBehavior, |
||||||
|
object? |
||||||
|
>(nameof(Context)); |
||||||
|
|
||||||
|
public static readonly StyledProperty<IDragHandler?> HandlerProperty = AvaloniaProperty.Register< |
||||||
|
ContextDragBehavior, |
||||||
|
IDragHandler? |
||||||
|
>(nameof(Handler)); |
||||||
|
|
||||||
|
public static readonly StyledProperty<double> HorizontalDragThresholdProperty = AvaloniaProperty.Register< |
||||||
|
ContextDragBehavior, |
||||||
|
double |
||||||
|
>(nameof(HorizontalDragThreshold), 3); |
||||||
|
|
||||||
|
public static readonly StyledProperty<double> VerticalDragThresholdProperty = AvaloniaProperty.Register< |
||||||
|
ContextDragBehavior, |
||||||
|
double |
||||||
|
>(nameof(VerticalDragThreshold), 3); |
||||||
|
|
||||||
|
public static readonly StyledProperty<string> DataFormatProperty = AvaloniaProperty.Register< |
||||||
|
BetterContextDragBehavior, |
||||||
|
string |
||||||
|
>("DataFormat"); |
||||||
|
|
||||||
|
public string DataFormat |
||||||
|
{ |
||||||
|
get => GetValue(DataFormatProperty); |
||||||
|
set => SetValue(DataFormatProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public object? Context |
||||||
|
{ |
||||||
|
get => GetValue(ContextProperty); |
||||||
|
set => SetValue(ContextProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public IDragHandler? Handler |
||||||
|
{ |
||||||
|
get => GetValue(HandlerProperty); |
||||||
|
set => SetValue(HandlerProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public double HorizontalDragThreshold |
||||||
|
{ |
||||||
|
get => GetValue(HorizontalDragThresholdProperty); |
||||||
|
set => SetValue(HorizontalDragThresholdProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public double VerticalDragThreshold |
||||||
|
{ |
||||||
|
get => GetValue(VerticalDragThresholdProperty); |
||||||
|
set => SetValue(VerticalDragThresholdProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
protected override void OnAttachedToVisualTree() |
||||||
|
{ |
||||||
|
AssociatedObject?.AddHandler( |
||||||
|
InputElement.PointerPressedEvent, |
||||||
|
AssociatedObject_PointerPressed, |
||||||
|
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble |
||||||
|
); |
||||||
|
AssociatedObject?.AddHandler( |
||||||
|
InputElement.PointerReleasedEvent, |
||||||
|
AssociatedObject_PointerReleased, |
||||||
|
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble |
||||||
|
); |
||||||
|
AssociatedObject?.AddHandler( |
||||||
|
InputElement.PointerMovedEvent, |
||||||
|
AssociatedObject_PointerMoved, |
||||||
|
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble |
||||||
|
); |
||||||
|
AssociatedObject?.AddHandler( |
||||||
|
InputElement.PointerCaptureLostEvent, |
||||||
|
AssociatedObject_CaptureLost, |
||||||
|
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
protected override void OnDetachedFromVisualTree() |
||||||
|
{ |
||||||
|
AssociatedObject?.RemoveHandler(InputElement.PointerPressedEvent, AssociatedObject_PointerPressed); |
||||||
|
AssociatedObject?.RemoveHandler(InputElement.PointerReleasedEvent, AssociatedObject_PointerReleased); |
||||||
|
AssociatedObject?.RemoveHandler(InputElement.PointerMovedEvent, AssociatedObject_PointerMoved); |
||||||
|
AssociatedObject?.RemoveHandler(InputElement.PointerCaptureLostEvent, AssociatedObject_CaptureLost); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task DoDragDrop(PointerEventArgs triggerEvent, object? value) |
||||||
|
{ |
||||||
|
var data = new DataObject(); |
||||||
|
data.Set(DataFormat, value!); |
||||||
|
|
||||||
|
var effect = DragDropEffects.None; |
||||||
|
|
||||||
|
if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Alt)) |
||||||
|
{ |
||||||
|
effect |= DragDropEffects.Link; |
||||||
|
} |
||||||
|
else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Shift)) |
||||||
|
{ |
||||||
|
effect |= DragDropEffects.Move; |
||||||
|
} |
||||||
|
else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Control)) |
||||||
|
{ |
||||||
|
effect |= DragDropEffects.Copy; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
effect |= DragDropEffects.Move; |
||||||
|
} |
||||||
|
|
||||||
|
await DragDrop.DoDragDrop(triggerEvent, data, effect); |
||||||
|
} |
||||||
|
|
||||||
|
private void Released() |
||||||
|
{ |
||||||
|
_triggerEvent = null; |
||||||
|
_lock = false; |
||||||
|
} |
||||||
|
|
||||||
|
private void AssociatedObject_PointerPressed(object? sender, PointerPressedEventArgs e) |
||||||
|
{ |
||||||
|
var properties = e.GetCurrentPoint(AssociatedObject).Properties; |
||||||
|
if (properties.IsLeftButtonPressed) |
||||||
|
{ |
||||||
|
if (e.Source is Control control && AssociatedObject?.DataContext == control.DataContext) |
||||||
|
{ |
||||||
|
_dragStartPoint = e.GetPosition(null); |
||||||
|
_triggerEvent = e; |
||||||
|
_lock = true; |
||||||
|
_captured = true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void AssociatedObject_PointerReleased(object? sender, PointerReleasedEventArgs e) |
||||||
|
{ |
||||||
|
if (_captured) |
||||||
|
{ |
||||||
|
if (e.InitialPressMouseButton == MouseButton.Left && _triggerEvent is { }) |
||||||
|
{ |
||||||
|
Released(); |
||||||
|
} |
||||||
|
|
||||||
|
_captured = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async void AssociatedObject_PointerMoved(object? sender, PointerEventArgs e) |
||||||
|
{ |
||||||
|
var properties = e.GetCurrentPoint(AssociatedObject).Properties; |
||||||
|
if (_captured && properties.IsLeftButtonPressed && _triggerEvent is { }) |
||||||
|
{ |
||||||
|
var point = e.GetPosition(null); |
||||||
|
var diff = _dragStartPoint - point; |
||||||
|
var horizontalDragThreshold = HorizontalDragThreshold; |
||||||
|
var verticalDragThreshold = VerticalDragThreshold; |
||||||
|
|
||||||
|
if (Math.Abs(diff.X) > horizontalDragThreshold || Math.Abs(diff.Y) > verticalDragThreshold) |
||||||
|
{ |
||||||
|
if (_lock) |
||||||
|
{ |
||||||
|
_lock = false; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var context = Context ?? AssociatedObject?.DataContext; |
||||||
|
|
||||||
|
Handler?.BeforeDragDrop(sender, _triggerEvent, context); |
||||||
|
|
||||||
|
await DoDragDrop(_triggerEvent, context); |
||||||
|
|
||||||
|
Handler?.AfterDragDrop(sender, _triggerEvent, context); |
||||||
|
|
||||||
|
_triggerEvent = null; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void AssociatedObject_CaptureLost(object? sender, PointerCaptureLostEventArgs e) |
||||||
|
{ |
||||||
|
Released(); |
||||||
|
_captured = false; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
<Styles |
||||||
|
xmlns="https://github.com/avaloniaui" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||||
|
xmlns:controls="using:StabilityMatrix.Avalonia.Controls" |
||||||
|
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia" |
||||||
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||||
|
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" |
||||||
|
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" |
||||||
|
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||||
|
x:DataType="vmInference:LayerDiffuseCardViewModel"> |
||||||
|
<Design.PreviewWith> |
||||||
|
<Panel Width="400" Height="200"> |
||||||
|
<StackPanel Width="300" VerticalAlignment="Center"> |
||||||
|
<controls:LayerDiffuseCard DataContext="{x:Static mocks:DesignData.LayerDiffuseCardViewModel}"/> |
||||||
|
</StackPanel> |
||||||
|
</Panel> |
||||||
|
</Design.PreviewWith> |
||||||
|
|
||||||
|
<Style Selector="controls|LayerDiffuseCard"> |
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch" /> |
||||||
|
<Setter Property="Template"> |
||||||
|
<ControlTemplate> |
||||||
|
<controls:Card Padding="12"> |
||||||
|
<sg:SpacedGrid |
||||||
|
ColumnDefinitions="Auto,*" |
||||||
|
ColumnSpacing="8" |
||||||
|
RowDefinitions="*,*,*,*" |
||||||
|
RowSpacing="0"> |
||||||
|
<!-- Mode Selection --> |
||||||
|
<TextBlock |
||||||
|
Grid.Column="0" |
||||||
|
VerticalAlignment="Center" |
||||||
|
Text="Mode" |
||||||
|
TextAlignment="Left" /> |
||||||
|
|
||||||
|
<ui:FAComboBox |
||||||
|
Grid.Row="0" |
||||||
|
Grid.Column="1" |
||||||
|
HorizontalAlignment="Stretch" |
||||||
|
DisplayMemberBinding="{Binding Converter={x:Static converters:EnumAttributeConverters.DisplayName}}" |
||||||
|
ItemsSource="{Binding AvailableModes}" |
||||||
|
SelectedItem="{Binding SelectedMode}" /> |
||||||
|
</sg:SpacedGrid> |
||||||
|
</controls:Card> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
</Styles> |
@ -0,0 +1,7 @@ |
|||||||
|
using Avalonia.Controls.Primitives; |
||||||
|
using StabilityMatrix.Core.Attributes; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls; |
||||||
|
|
||||||
|
[Transient] |
||||||
|
public class LayerDiffuseCard : TemplatedControl; |
@ -0,0 +1,20 @@ |
|||||||
|
using System; |
||||||
|
using Avalonia.Interactivity; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs; |
||||||
|
|
||||||
|
public partial class BetterAsyncImage |
||||||
|
{ |
||||||
|
public class AsyncImageFailedEventArgs : RoutedEventArgs |
||||||
|
{ |
||||||
|
internal AsyncImageFailedEventArgs(Exception? errorException = null, string errorMessage = "") |
||||||
|
: base(FailedEvent) |
||||||
|
{ |
||||||
|
ErrorException = errorException; |
||||||
|
ErrorMessage = errorMessage; |
||||||
|
} |
||||||
|
|
||||||
|
public Exception? ErrorException { get; private set; } |
||||||
|
public string ErrorMessage { get; private set; } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
using System; |
||||||
|
using Avalonia.Interactivity; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs; |
||||||
|
|
||||||
|
public partial class BetterAsyncImage |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Deines the <see cref="Opened"/> event |
||||||
|
/// </summary> |
||||||
|
public static readonly RoutedEvent<RoutedEventArgs> OpenedEvent = RoutedEvent.Register< |
||||||
|
BetterAsyncImage, |
||||||
|
RoutedEventArgs |
||||||
|
>(nameof(Opened), RoutingStrategies.Bubble); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Deines the <see cref="Failed"/> event |
||||||
|
/// </summary> |
||||||
|
public static readonly RoutedEvent<global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs> FailedEvent = |
||||||
|
RoutedEvent.Register< |
||||||
|
BetterAsyncImage, |
||||||
|
global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs |
||||||
|
>(nameof(Failed), RoutingStrategies.Bubble); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Occurs when the image is successfully loaded. |
||||||
|
/// </summary> |
||||||
|
public event EventHandler<RoutedEventArgs>? Opened |
||||||
|
{ |
||||||
|
add => AddHandler(OpenedEvent, value); |
||||||
|
remove => RemoveHandler(OpenedEvent, value); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Occurs when the image fails to load the uri provided. |
||||||
|
/// </summary> |
||||||
|
public event EventHandler<global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs>? Failed |
||||||
|
{ |
||||||
|
add => AddHandler(FailedEvent, value); |
||||||
|
remove => RemoveHandler(FailedEvent, value); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,135 @@ |
|||||||
|
using System; |
||||||
|
using Avalonia; |
||||||
|
using Avalonia.Animation; |
||||||
|
using Avalonia.Labs.Controls; |
||||||
|
using Avalonia.Media; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs; |
||||||
|
|
||||||
|
public partial class BetterAsyncImage |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="PlaceholderSource"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly StyledProperty<IImage?> PlaceholderSourceProperty = AvaloniaProperty.Register< |
||||||
|
BetterAsyncImage, |
||||||
|
IImage? |
||||||
|
>(nameof(PlaceholderSource)); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="Source"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly StyledProperty<Uri?> SourceProperty = AvaloniaProperty.Register< |
||||||
|
BetterAsyncImage, |
||||||
|
Uri? |
||||||
|
>(nameof(Source)); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="Stretch"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register< |
||||||
|
BetterAsyncImage, |
||||||
|
Stretch |
||||||
|
>(nameof(Stretch), Stretch.Uniform); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="PlaceholderStretch"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly StyledProperty<Stretch> PlaceholderStretchProperty = AvaloniaProperty.Register< |
||||||
|
BetterAsyncImage, |
||||||
|
Stretch |
||||||
|
>(nameof(PlaceholderStretch), Stretch.Uniform); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="State"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly DirectProperty<BetterAsyncImage, AsyncImageState> StateProperty = |
||||||
|
AvaloniaProperty.RegisterDirect<BetterAsyncImage, AsyncImageState>( |
||||||
|
nameof(State), |
||||||
|
o => o.State, |
||||||
|
(o, v) => o.State = v |
||||||
|
); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="ImageTransition"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly StyledProperty<IPageTransition?> ImageTransitionProperty = |
||||||
|
AvaloniaProperty.Register<BetterAsyncImage, IPageTransition?>( |
||||||
|
nameof(ImageTransition), |
||||||
|
new CrossFade(TimeSpan.FromSeconds(0.25)) |
||||||
|
); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Defines the <see cref="IsCacheEnabled"/> property. |
||||||
|
/// </summary> |
||||||
|
public static readonly DirectProperty<BetterAsyncImage, bool> IsCacheEnabledProperty = |
||||||
|
AvaloniaProperty.RegisterDirect<BetterAsyncImage, bool>( |
||||||
|
nameof(IsCacheEnabled), |
||||||
|
o => o.IsCacheEnabled, |
||||||
|
(o, v) => o.IsCacheEnabled = v |
||||||
|
); |
||||||
|
private bool _isCacheEnabled; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets the placeholder image. |
||||||
|
/// </summary> |
||||||
|
public IImage? PlaceholderSource |
||||||
|
{ |
||||||
|
get => GetValue(PlaceholderSourceProperty); |
||||||
|
set => SetValue(PlaceholderSourceProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets the uri pointing to the image resource |
||||||
|
/// </summary> |
||||||
|
public Uri? Source |
||||||
|
{ |
||||||
|
get => GetValue(SourceProperty); |
||||||
|
set => SetValue(SourceProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets a value controlling how the image will be stretched. |
||||||
|
/// </summary> |
||||||
|
public Stretch Stretch |
||||||
|
{ |
||||||
|
get { return GetValue(StretchProperty); } |
||||||
|
set { SetValue(StretchProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets a value controlling how the placeholder will be stretched. |
||||||
|
/// </summary> |
||||||
|
public Stretch PlaceholderStretch |
||||||
|
{ |
||||||
|
get { return GetValue(StretchProperty); } |
||||||
|
set { SetValue(StretchProperty, value); } |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets the current loading state of the image. |
||||||
|
/// </summary> |
||||||
|
public AsyncImageState State |
||||||
|
{ |
||||||
|
get => _state; |
||||||
|
private set => SetAndRaise(StateProperty, ref _state, value); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets the transition to run when the image is loaded. |
||||||
|
/// </summary> |
||||||
|
public IPageTransition? ImageTransition |
||||||
|
{ |
||||||
|
get => GetValue(ImageTransitionProperty); |
||||||
|
set => SetValue(ImageTransitionProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets whether to use cache for retrieved images |
||||||
|
/// </summary> |
||||||
|
public bool IsCacheEnabled |
||||||
|
{ |
||||||
|
get => _isCacheEnabled; |
||||||
|
set => SetAndRaise(IsCacheEnabledProperty, ref _isCacheEnabled, value); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,248 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Net.Http; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using Avalonia; |
||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.Controls.Metadata; |
||||||
|
using Avalonia.Controls.Primitives; |
||||||
|
using Avalonia.Interactivity; |
||||||
|
using Avalonia.Labs.Controls; |
||||||
|
using Avalonia.Media; |
||||||
|
using Avalonia.Media.Imaging; |
||||||
|
using Avalonia.Platform; |
||||||
|
using Avalonia.Threading; |
||||||
|
using StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// An image control that asynchronously retrieves an image using a <see cref="Uri"/>. |
||||||
|
/// </summary> |
||||||
|
[TemplatePart("PART_Image", typeof(Image))] |
||||||
|
[TemplatePart("PART_PlaceholderImage", typeof(Image))] |
||||||
|
public partial class BetterAsyncImage : TemplatedControl |
||||||
|
{ |
||||||
|
protected Image? ImagePart { get; private set; } |
||||||
|
protected Image? PlaceholderPart { get; private set; } |
||||||
|
|
||||||
|
private bool _isInitialized; |
||||||
|
private CancellationTokenSource? _tokenSource; |
||||||
|
private AsyncImageState _state; |
||||||
|
|
||||||
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(e); |
||||||
|
|
||||||
|
ImagePart = e.NameScope.Get<Image>("PART_Image"); |
||||||
|
PlaceholderPart = e.NameScope.Get<Image>("PART_PlaceholderImage"); |
||||||
|
|
||||||
|
_tokenSource = new CancellationTokenSource(); |
||||||
|
|
||||||
|
_isInitialized = true; |
||||||
|
|
||||||
|
if (Source != null) |
||||||
|
{ |
||||||
|
SetSource(Source); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async void SetSource(object? source) |
||||||
|
{ |
||||||
|
if (!_isInitialized) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
_tokenSource?.Cancel(); |
||||||
|
|
||||||
|
_tokenSource = new CancellationTokenSource(); |
||||||
|
|
||||||
|
AttachSource(null); |
||||||
|
|
||||||
|
if (source == null) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
State = AsyncImageState.Loading; |
||||||
|
|
||||||
|
if (Source is IImage image) |
||||||
|
{ |
||||||
|
AttachSource(image); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (Source == null) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var uri = Source; |
||||||
|
|
||||||
|
if (uri != null && uri.IsAbsoluteUri) |
||||||
|
{ |
||||||
|
if (uri.Scheme == "http" || uri.Scheme == "https") |
||||||
|
{ |
||||||
|
Bitmap? bitmap = null; |
||||||
|
// Android doesn't allow network requests on the main thread, even though we are using async apis. |
||||||
|
#if NET6_0_OR_GREATER |
||||||
|
if (OperatingSystem.IsAndroid()) |
||||||
|
{ |
||||||
|
await Task.Run(async () => |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
bitmap = await LoadImageAsync(uri, _tokenSource.Token); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => |
||||||
|
{ |
||||||
|
State = AsyncImageState.Failed; |
||||||
|
|
||||||
|
RaiseEvent(new AsyncImageFailedEventArgs(ex)); |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
else |
||||||
|
#endif |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
bitmap = await LoadImageAsync(uri, _tokenSource.Token); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => |
||||||
|
{ |
||||||
|
State = AsyncImageState.Failed; |
||||||
|
|
||||||
|
RaiseEvent(new AsyncImageFailedEventArgs(ex)); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
AttachSource(bitmap); |
||||||
|
} |
||||||
|
else if (uri.Scheme == "avares") |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
AttachSource(new Bitmap(AssetLoader.Open(uri))); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
State = AsyncImageState.Failed; |
||||||
|
|
||||||
|
RaiseEvent(new AsyncImageFailedEventArgs(ex)); |
||||||
|
} |
||||||
|
} |
||||||
|
else if (uri.Scheme == "file" && File.Exists(uri.LocalPath)) |
||||||
|
{ |
||||||
|
// Added error handling here for local files |
||||||
|
try |
||||||
|
{ |
||||||
|
AttachSource(new Bitmap(uri.LocalPath)); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
State = AsyncImageState.Failed; |
||||||
|
|
||||||
|
RaiseEvent(new AsyncImageFailedEventArgs(ex)); |
||||||
|
} |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
RaiseEvent( |
||||||
|
new AsyncImageFailedEventArgs( |
||||||
|
new UriFormatException($"Uri has unsupported scheme. Uri:{source}") |
||||||
|
) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
RaiseEvent( |
||||||
|
new AsyncImageFailedEventArgs( |
||||||
|
new UriFormatException($"Relative paths aren't supported. Uri:{source}") |
||||||
|
) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void AttachSource(IImage? image) |
||||||
|
{ |
||||||
|
if (ImagePart != null) |
||||||
|
{ |
||||||
|
ImagePart.Source = image; |
||||||
|
} |
||||||
|
|
||||||
|
_tokenSource?.Cancel(); |
||||||
|
_tokenSource = new CancellationTokenSource(); |
||||||
|
|
||||||
|
if (image == null) |
||||||
|
{ |
||||||
|
State = AsyncImageState.Unloaded; |
||||||
|
|
||||||
|
ImageTransition?.Start(ImagePart, PlaceholderPart, true, _tokenSource.Token); |
||||||
|
} |
||||||
|
else if (image.Size != default) |
||||||
|
{ |
||||||
|
State = AsyncImageState.Loaded; |
||||||
|
|
||||||
|
ImageTransition?.Start(PlaceholderPart, ImagePart, true, _tokenSource.Token); |
||||||
|
|
||||||
|
RaiseEvent(new RoutedEventArgs(OpenedEvent)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Bitmap> LoadImageAsync(Uri? url, CancellationToken token) |
||||||
|
{ |
||||||
|
if (await ProvideCachedResourceAsync(url, token) is { } bitmap) |
||||||
|
{ |
||||||
|
return bitmap; |
||||||
|
} |
||||||
|
#if NET6_0_OR_GREATER |
||||||
|
using var client = new HttpClient(); |
||||||
|
var stream = await client.GetStreamAsync(url, token).ConfigureAwait(false); |
||||||
|
|
||||||
|
await using var memoryStream = new MemoryStream(); |
||||||
|
await stream.CopyToAsync(memoryStream, token).ConfigureAwait(false); |
||||||
|
#elif NETSTANDARD2_0 |
||||||
|
using var client = new HttpClient(); |
||||||
|
var response = await client.GetAsync(url, token).ConfigureAwait(false); |
||||||
|
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); |
||||||
|
|
||||||
|
using var memoryStream = new MemoryStream(); |
||||||
|
await stream.CopyToAsync(memoryStream).ConfigureAwait(false); |
||||||
|
#endif |
||||||
|
|
||||||
|
memoryStream.Position = 0; |
||||||
|
return new Bitmap(memoryStream); |
||||||
|
} |
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
||||||
|
{ |
||||||
|
base.OnPropertyChanged(change); |
||||||
|
|
||||||
|
if (change.Property == SourceProperty) |
||||||
|
{ |
||||||
|
SetSource(Source); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected virtual async Task<Bitmap?> ProvideCachedResourceAsync(Uri? imageUri, CancellationToken token) |
||||||
|
{ |
||||||
|
if (IsCacheEnabled && imageUri != null) |
||||||
|
{ |
||||||
|
return await ImageCache |
||||||
|
.Instance.GetFromCacheAsync(imageUri, cancellationToken: token) |
||||||
|
.ConfigureAwait(false); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,565 @@ |
|||||||
|
// Parts of this file was taken from Windows Community Toolkit CacheBase implementation |
||||||
|
// https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp.UI/Cache/ImageCache.cs |
||||||
|
|
||||||
|
// Licensed to the .NET Foundation under one or more agreements. |
||||||
|
// The .NET Foundation licenses this file to you under the MIT license. |
||||||
|
// See the LICENSE file in the project root for more information. |
||||||
|
|
||||||
|
using System; |
||||||
|
using System.Collections.Concurrent; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Net.Http; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
internal abstract class CacheBase<T> |
||||||
|
{ |
||||||
|
private class ConcurrentRequest |
||||||
|
{ |
||||||
|
public Task<T?>? Task { get; set; } |
||||||
|
|
||||||
|
public bool EnsureCachedCopy { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
private readonly SemaphoreSlim _cacheFolderSemaphore = new SemaphoreSlim(1); |
||||||
|
private string? _baseFolder = null; |
||||||
|
private string? _cacheFolderName = null; |
||||||
|
|
||||||
|
private string? _cacheFolder = null; |
||||||
|
private InMemoryStorage<T>? _inMemoryFileStorage = null; |
||||||
|
|
||||||
|
private ConcurrentDictionary<string, ConcurrentRequest> _concurrentTasks = |
||||||
|
new ConcurrentDictionary<string, ConcurrentRequest>(); |
||||||
|
|
||||||
|
private HttpClient? _httpClient = null; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Initializes a new instance of the <see cref="CacheBase{T}"/> class. |
||||||
|
/// </summary> |
||||||
|
protected CacheBase() |
||||||
|
{ |
||||||
|
var options = CacheOptions.Default; |
||||||
|
CacheDuration = options?.CacheDuration ?? TimeSpan.FromDays(1); |
||||||
|
_baseFolder = options?.BaseCachePath ?? null; |
||||||
|
_inMemoryFileStorage = new InMemoryStorage<T>(); |
||||||
|
RetryCount = 1; |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets the life duration of every cache entry. |
||||||
|
/// </summary> |
||||||
|
public TimeSpan CacheDuration { get; set; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets the number of retries trying to ensure the file is cached. |
||||||
|
/// </summary> |
||||||
|
public uint RetryCount { get; set; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets max in-memory item storage count |
||||||
|
/// </summary> |
||||||
|
public int MaxMemoryCacheCount |
||||||
|
{ |
||||||
|
get { return _inMemoryFileStorage?.MaxItemCount ?? 0; } |
||||||
|
set |
||||||
|
{ |
||||||
|
if (_inMemoryFileStorage != null) |
||||||
|
_inMemoryFileStorage.MaxItemCount = value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets instance of <see cref="HttpClient"/> |
||||||
|
/// </summary> |
||||||
|
protected HttpClient HttpClient |
||||||
|
{ |
||||||
|
get |
||||||
|
{ |
||||||
|
if (_httpClient == null) |
||||||
|
{ |
||||||
|
var messageHandler = new HttpClientHandler(); |
||||||
|
|
||||||
|
_httpClient = new HttpClient(messageHandler); |
||||||
|
} |
||||||
|
|
||||||
|
return _httpClient; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Initializes FileCache and provides root folder and cache folder name |
||||||
|
/// </summary> |
||||||
|
/// <param name="folder">Folder that is used as root for cache</param> |
||||||
|
/// <param name="folderName">Cache folder name</param> |
||||||
|
/// <param name="httpMessageHandler">instance of <see cref="HttpMessageHandler"/></param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
public virtual async Task InitializeAsync( |
||||||
|
string? folder = null, |
||||||
|
string? folderName = null, |
||||||
|
HttpMessageHandler? httpMessageHandler = null |
||||||
|
) |
||||||
|
{ |
||||||
|
_baseFolder = folder; |
||||||
|
_cacheFolderName = folderName; |
||||||
|
|
||||||
|
_cacheFolder = await GetCacheFolderAsync().ConfigureAwait(false); |
||||||
|
|
||||||
|
if (httpMessageHandler != null) |
||||||
|
{ |
||||||
|
_httpClient = new HttpClient(httpMessageHandler); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Clears all files in the cache |
||||||
|
/// </summary> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
public async Task ClearAsync() |
||||||
|
{ |
||||||
|
var folder = await GetCacheFolderAsync().ConfigureAwait(false); |
||||||
|
var files = Directory.EnumerateFiles(folder!); |
||||||
|
|
||||||
|
await InternalClearAsync(files.Select(x => x as string)).ConfigureAwait(false); |
||||||
|
|
||||||
|
_inMemoryFileStorage?.Clear(); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Clears file if it has expired |
||||||
|
/// </summary> |
||||||
|
/// <param name="duration">timespan to compute whether file has expired or not</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
public Task ClearAsync(TimeSpan duration) |
||||||
|
{ |
||||||
|
return RemoveExpiredAsync(duration); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Removes cached files that have expired |
||||||
|
/// </summary> |
||||||
|
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
public async Task RemoveExpiredAsync(TimeSpan? duration = null) |
||||||
|
{ |
||||||
|
TimeSpan expiryDuration = duration ?? CacheDuration; |
||||||
|
|
||||||
|
var folder = await GetCacheFolderAsync().ConfigureAwait(false); |
||||||
|
var files = Directory.EnumerateFiles(folder!); |
||||||
|
|
||||||
|
var filesToDelete = new List<string>(); |
||||||
|
|
||||||
|
foreach (var file in files) |
||||||
|
{ |
||||||
|
if (file == null) |
||||||
|
{ |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false)) |
||||||
|
{ |
||||||
|
filesToDelete.Add(file); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
await InternalClearAsync(filesToDelete).ConfigureAwait(false); |
||||||
|
|
||||||
|
_inMemoryFileStorage?.Clear(expiryDuration); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Removed items based on uri list passed |
||||||
|
/// </summary> |
||||||
|
/// <param name="uriForCachedItems">Enumerable uri list</param> |
||||||
|
/// <returns>awaitable Task</returns> |
||||||
|
public async Task RemoveAsync(IEnumerable<Uri> uriForCachedItems) |
||||||
|
{ |
||||||
|
if (uriForCachedItems == null || !uriForCachedItems.Any()) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var folder = await GetCacheFolderAsync().ConfigureAwait(false); |
||||||
|
var files = Directory.EnumerateFiles(folder!); |
||||||
|
var filesToDelete = new List<string>(); |
||||||
|
var keys = new List<string>(); |
||||||
|
|
||||||
|
Dictionary<string, string> hashDictionary = new Dictionary<string, string>(); |
||||||
|
|
||||||
|
foreach (var file in files) |
||||||
|
{ |
||||||
|
hashDictionary.Add(Path.GetFileName(file), file); |
||||||
|
} |
||||||
|
|
||||||
|
foreach (var uri in uriForCachedItems) |
||||||
|
{ |
||||||
|
string fileName = GetCacheFileName(uri); |
||||||
|
if (hashDictionary.TryGetValue(fileName, out var file)) |
||||||
|
{ |
||||||
|
filesToDelete.Add(file); |
||||||
|
keys.Add(fileName); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
await InternalClearAsync(filesToDelete).ConfigureAwait(false); |
||||||
|
|
||||||
|
_inMemoryFileStorage?.Remove(keys); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Assures that item represented by Uri is cached. |
||||||
|
/// </summary> |
||||||
|
/// <param name="uri">Uri of the item</param> |
||||||
|
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be cached</param> |
||||||
|
/// <param name="storeToMemoryCache">Indicates if item should be loaded into the in-memory storage</param> |
||||||
|
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param> |
||||||
|
/// <returns>Awaitable Task</returns> |
||||||
|
public Task PreCacheAsync( |
||||||
|
Uri uri, |
||||||
|
bool throwOnError = false, |
||||||
|
bool storeToMemoryCache = false, |
||||||
|
CancellationToken cancellationToken = default(CancellationToken) |
||||||
|
) |
||||||
|
{ |
||||||
|
return GetItemAsync(uri, throwOnError, !storeToMemoryCache, cancellationToken); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Retrieves item represented by Uri from the cache. If the item is not found in the cache, it will try to downloaded and saved before returning it to the caller. |
||||||
|
/// </summary> |
||||||
|
/// <param name="uri">Uri of the item.</param> |
||||||
|
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be found / downloaded.</param> |
||||||
|
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param> |
||||||
|
/// <returns>an instance of Generic type</returns> |
||||||
|
public Task<T?> GetFromCacheAsync( |
||||||
|
Uri uri, |
||||||
|
bool throwOnError = false, |
||||||
|
CancellationToken cancellationToken = default(CancellationToken) |
||||||
|
) |
||||||
|
{ |
||||||
|
return GetItemAsync(uri, throwOnError, false, cancellationToken); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets the string containing cached item for given Uri |
||||||
|
/// </summary> |
||||||
|
/// <param name="uri">Uri of the item.</param> |
||||||
|
/// <returns>a string</returns> |
||||||
|
public async Task<string> GetFileFromCacheAsync(Uri uri) |
||||||
|
{ |
||||||
|
var folder = await GetCacheFolderAsync().ConfigureAwait(false); |
||||||
|
|
||||||
|
return Path.Combine(folder!, GetCacheFileName(uri)); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Retrieves item represented by Uri from the in-memory cache if it exists and is not out of date. If item is not found or is out of date, default instance of the generic type is returned. |
||||||
|
/// </summary> |
||||||
|
/// <param name="uri">Uri of the item.</param> |
||||||
|
/// <returns>an instance of Generic type</returns> |
||||||
|
public T? GetFromMemoryCache(Uri uri) |
||||||
|
{ |
||||||
|
T? instance = default(T); |
||||||
|
|
||||||
|
string fileName = GetCacheFileName(uri); |
||||||
|
|
||||||
|
if (_inMemoryFileStorage?.MaxItemCount > 0) |
||||||
|
{ |
||||||
|
var msi = _inMemoryFileStorage?.GetItem(fileName, CacheDuration); |
||||||
|
if (msi != null) |
||||||
|
{ |
||||||
|
instance = msi.Item; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return instance; |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Cache specific hooks to process items from HTTP response |
||||||
|
/// </summary> |
||||||
|
/// <param name="stream">input stream</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
protected abstract Task<T> ConvertFromAsync(Stream stream); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Cache specific hooks to process items from HTTP response |
||||||
|
/// </summary> |
||||||
|
/// <param name="baseFile">storage file</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
protected abstract Task<T> ConvertFromAsync(string baseFile); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Override-able method that checks whether file is valid or not. |
||||||
|
/// </summary> |
||||||
|
/// <param name="file">storage file</param> |
||||||
|
/// <param name="duration">cache duration</param> |
||||||
|
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param> |
||||||
|
/// <returns>bool indicate whether file has expired or not</returns> |
||||||
|
protected virtual async Task<bool> IsFileOutOfDateAsync( |
||||||
|
string file, |
||||||
|
TimeSpan duration, |
||||||
|
bool treatNullFileAsOutOfDate = true |
||||||
|
) |
||||||
|
{ |
||||||
|
if (file == null) |
||||||
|
{ |
||||||
|
return treatNullFileAsOutOfDate; |
||||||
|
} |
||||||
|
|
||||||
|
var info = new FileInfo(file); |
||||||
|
|
||||||
|
return info.Length == 0 || DateTime.Now.Subtract(info.LastWriteTime) > duration; |
||||||
|
} |
||||||
|
|
||||||
|
private static string GetCacheFileName(Uri uri) |
||||||
|
{ |
||||||
|
return CreateHash64(uri.ToString()).ToString(); |
||||||
|
} |
||||||
|
|
||||||
|
private static ulong CreateHash64(string str) |
||||||
|
{ |
||||||
|
byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(str); |
||||||
|
|
||||||
|
ulong value = (ulong)utf8.Length; |
||||||
|
for (int n = 0; n < utf8.Length; n++) |
||||||
|
{ |
||||||
|
value += (ulong)utf8[n] << ((n * 5) % 56); |
||||||
|
} |
||||||
|
|
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<T?> GetItemAsync( |
||||||
|
Uri uri, |
||||||
|
bool throwOnError, |
||||||
|
bool preCacheOnly, |
||||||
|
CancellationToken cancellationToken |
||||||
|
) |
||||||
|
{ |
||||||
|
T? instance = default(T); |
||||||
|
|
||||||
|
string fileName = GetCacheFileName(uri); |
||||||
|
_concurrentTasks.TryGetValue(fileName, out var request); |
||||||
|
|
||||||
|
// if similar request exists check if it was preCacheOnly and validate that current request isn't preCacheOnly |
||||||
|
if (request != null && request.EnsureCachedCopy && !preCacheOnly) |
||||||
|
{ |
||||||
|
if (request.Task != null) |
||||||
|
await request.Task.ConfigureAwait(false); |
||||||
|
request = null; |
||||||
|
} |
||||||
|
|
||||||
|
if (request == null) |
||||||
|
{ |
||||||
|
request = new ConcurrentRequest() |
||||||
|
{ |
||||||
|
Task = GetFromCacheOrDownloadAsync(uri, fileName, preCacheOnly, cancellationToken), |
||||||
|
EnsureCachedCopy = preCacheOnly |
||||||
|
}; |
||||||
|
|
||||||
|
_concurrentTasks[fileName] = request; |
||||||
|
} |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
if (request.Task != null) |
||||||
|
instance = await request.Task.ConfigureAwait(false); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
System.Diagnostics.Debug.WriteLine(ex.Message); |
||||||
|
if (throwOnError) |
||||||
|
{ |
||||||
|
throw; |
||||||
|
} |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
_concurrentTasks.TryRemove(fileName, out _); |
||||||
|
} |
||||||
|
|
||||||
|
return instance; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<T?> GetFromCacheOrDownloadAsync( |
||||||
|
Uri uri, |
||||||
|
string fileName, |
||||||
|
bool preCacheOnly, |
||||||
|
CancellationToken cancellationToken |
||||||
|
) |
||||||
|
{ |
||||||
|
T? instance = default(T); |
||||||
|
|
||||||
|
if (_inMemoryFileStorage?.MaxItemCount > 0) |
||||||
|
{ |
||||||
|
var msi = _inMemoryFileStorage?.GetItem(fileName, CacheDuration); |
||||||
|
if (msi != null) |
||||||
|
{ |
||||||
|
instance = msi.Item; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (instance != null) |
||||||
|
{ |
||||||
|
return instance; |
||||||
|
} |
||||||
|
|
||||||
|
var folder = await GetCacheFolderAsync().ConfigureAwait(false); |
||||||
|
var baseFile = Path.Combine(folder!, fileName); |
||||||
|
|
||||||
|
bool downloadDataFile = |
||||||
|
!File.Exists(baseFile) |
||||||
|
|| await IsFileOutOfDateAsync(baseFile, CacheDuration).ConfigureAwait(false); |
||||||
|
|
||||||
|
if (!File.Exists(baseFile)) |
||||||
|
{ |
||||||
|
File.Create(baseFile).Dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
if (downloadDataFile) |
||||||
|
{ |
||||||
|
uint retries = 0; |
||||||
|
try |
||||||
|
{ |
||||||
|
while (retries < RetryCount) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
instance = await DownloadFileAsync(uri, baseFile, preCacheOnly, cancellationToken) |
||||||
|
.ConfigureAwait(false); |
||||||
|
|
||||||
|
if (instance != null) |
||||||
|
{ |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
catch (FileNotFoundException) { } |
||||||
|
|
||||||
|
retries++; |
||||||
|
} |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
File.Delete(baseFile); |
||||||
|
throw; // re-throwing the exception changes the stack trace. just throw |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (EqualityComparer<T>.Default.Equals(instance, default(T)) && !preCacheOnly) |
||||||
|
{ |
||||||
|
instance = await ConvertFromAsync(baseFile).ConfigureAwait(false); |
||||||
|
|
||||||
|
if (_inMemoryFileStorage?.MaxItemCount > 0) |
||||||
|
{ |
||||||
|
var properties = new FileInfo(baseFile); |
||||||
|
|
||||||
|
var msi = new InMemoryStorageItem<T>(fileName, properties.LastWriteTime, instance); |
||||||
|
_inMemoryFileStorage?.SetItem(msi); |
||||||
|
} |
||||||
|
} |
||||||
|
return instance; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<T?> DownloadFileAsync( |
||||||
|
Uri uri, |
||||||
|
string baseFile, |
||||||
|
bool preCacheOnly, |
||||||
|
CancellationToken cancellationToken |
||||||
|
) |
||||||
|
{ |
||||||
|
T? instance = default(T); |
||||||
|
|
||||||
|
using (MemoryStream ms = new MemoryStream()) |
||||||
|
{ |
||||||
|
using (var stream = await HttpClient.GetStreamAsync(uri)) |
||||||
|
{ |
||||||
|
stream.CopyTo(ms); |
||||||
|
ms.Flush(); |
||||||
|
|
||||||
|
ms.Position = 0; |
||||||
|
|
||||||
|
using (var fs = File.Open(baseFile, FileMode.OpenOrCreate, FileAccess.Write)) |
||||||
|
{ |
||||||
|
ms.CopyTo(fs); |
||||||
|
|
||||||
|
fs.Flush(); |
||||||
|
|
||||||
|
ms.Position = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// if its pre-cache we aren't looking to load items in memory |
||||||
|
if (!preCacheOnly) |
||||||
|
{ |
||||||
|
instance = await ConvertFromAsync(ms).ConfigureAwait(false); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return instance; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task InternalClearAsync(IEnumerable<string?> files) |
||||||
|
{ |
||||||
|
foreach (var file in files) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
File.Delete(file!); |
||||||
|
} |
||||||
|
catch |
||||||
|
{ |
||||||
|
// Just ignore errors for now} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Initializes with default values if user has not initialized explicitly |
||||||
|
/// </summary> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
private async Task ForceInitialiseAsync() |
||||||
|
{ |
||||||
|
if (_cacheFolder != null) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
await _cacheFolderSemaphore.WaitAsync().ConfigureAwait(false); |
||||||
|
|
||||||
|
_inMemoryFileStorage = new InMemoryStorage<T>(); |
||||||
|
|
||||||
|
if (_baseFolder == null) |
||||||
|
{ |
||||||
|
_baseFolder = Path.GetTempPath(); |
||||||
|
} |
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_cacheFolderName)) |
||||||
|
{ |
||||||
|
_cacheFolderName = GetType().Name; |
||||||
|
} |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
_cacheFolder = Path.Combine(_baseFolder, _cacheFolderName); |
||||||
|
Directory.CreateDirectory(_cacheFolder); |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
_cacheFolderSemaphore.Release(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<string?> GetCacheFolderAsync() |
||||||
|
{ |
||||||
|
if (_cacheFolder == null) |
||||||
|
{ |
||||||
|
await ForceInitialiseAsync().ConfigureAwait(false); |
||||||
|
} |
||||||
|
|
||||||
|
return _cacheFolder; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
using System; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
public class CacheOptions |
||||||
|
{ |
||||||
|
private static CacheOptions? _cacheOptions; |
||||||
|
|
||||||
|
public static CacheOptions Default => _cacheOptions ??= new CacheOptions(); |
||||||
|
|
||||||
|
public static void SetDefault(CacheOptions defaultCacheOptions) |
||||||
|
{ |
||||||
|
_cacheOptions = defaultCacheOptions; |
||||||
|
} |
||||||
|
|
||||||
|
public string? BaseCachePath { get; set; } |
||||||
|
public TimeSpan? CacheDuration { get; set; } |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
using System.IO; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Provides methods and tools to cache files in a folder |
||||||
|
/// </summary> |
||||||
|
internal class FileCache : CacheBase<string> |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Private singleton field. |
||||||
|
/// </summary> |
||||||
|
private static FileCache? _instance; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets public singleton property. |
||||||
|
/// </summary> |
||||||
|
public static FileCache Instance => _instance ?? (_instance = new FileCache()); |
||||||
|
|
||||||
|
protected override Task<string> ConvertFromAsync(Stream stream) |
||||||
|
{ |
||||||
|
// nothing to do in this instance; |
||||||
|
return Task.FromResult<string>(""); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Returns a cached path |
||||||
|
/// </summary> |
||||||
|
/// <param name="baseFile">storage file</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
protected override Task<string> ConvertFromAsync(string baseFile) |
||||||
|
{ |
||||||
|
return Task.FromResult(baseFile); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using Avalonia.Media.Imaging; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Provides methods and tools to cache images in a folder |
||||||
|
/// </summary> |
||||||
|
internal class ImageCache : CacheBase<Bitmap> |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Private singleton field. |
||||||
|
/// </summary> |
||||||
|
[ThreadStatic] |
||||||
|
private static ImageCache? _instance; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets public singleton property. |
||||||
|
/// </summary> |
||||||
|
public static ImageCache Instance => _instance ?? (_instance = new ImageCache()); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Creates a bitmap from a stream |
||||||
|
/// </summary> |
||||||
|
/// <param name="stream">input stream</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
protected override async Task<Bitmap> ConvertFromAsync(Stream stream) |
||||||
|
{ |
||||||
|
if (stream.Length == 0) |
||||||
|
{ |
||||||
|
throw new FileNotFoundException(); |
||||||
|
} |
||||||
|
|
||||||
|
return new Bitmap(stream); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Creates a bitmap from a cached file |
||||||
|
/// </summary> |
||||||
|
/// <param name="baseFile">file</param> |
||||||
|
/// <returns>awaitable task</returns> |
||||||
|
protected override async Task<Bitmap> ConvertFromAsync(string baseFile) |
||||||
|
{ |
||||||
|
using (var stream = File.OpenRead(baseFile)) |
||||||
|
{ |
||||||
|
return await ConvertFromAsync(stream).ConfigureAwait(false); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Checks whether file is valid or not. |
||||||
|
/// </summary> |
||||||
|
/// <param name="file">file</param> |
||||||
|
/// <param name="duration">cache duration</param> |
||||||
|
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param> |
||||||
|
/// <returns>bool indicate whether file has expired or not</returns> |
||||||
|
protected override async Task<bool> IsFileOutOfDateAsync( |
||||||
|
string file, |
||||||
|
TimeSpan duration, |
||||||
|
bool treatNullFileAsOutOfDate = true |
||||||
|
) |
||||||
|
{ |
||||||
|
if (file == null) |
||||||
|
{ |
||||||
|
return treatNullFileAsOutOfDate; |
||||||
|
} |
||||||
|
|
||||||
|
var fileInfo = new FileInfo(file); |
||||||
|
|
||||||
|
return fileInfo.Length == 0 |
||||||
|
|| DateTime.Now.Subtract(File.GetLastAccessTime(file)) > duration |
||||||
|
|| DateTime.Now.Subtract(File.GetLastWriteTime(file)) > duration; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,156 @@ |
|||||||
|
// Licensed to the .NET Foundation under one or more agreements. |
||||||
|
// The .NET Foundation licenses this file to you under the MIT license. |
||||||
|
// See the LICENSE file in the project root for more information. |
||||||
|
|
||||||
|
using System; |
||||||
|
using System.Collections.Concurrent; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using Avalonia.Labs.Controls.Cache; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Generic in-memory storage of items |
||||||
|
/// </summary> |
||||||
|
/// <typeparam name="T">T defines the type of item stored</typeparam> |
||||||
|
public class InMemoryStorage<T> |
||||||
|
{ |
||||||
|
private int _maxItemCount; |
||||||
|
private ConcurrentDictionary<string, InMemoryStorageItem<T>> _inMemoryStorage = |
||||||
|
new ConcurrentDictionary<string, InMemoryStorageItem<T>>(); |
||||||
|
private object _settingMaxItemCountLocker = new object(); |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets or sets the maximum count of Items that can be stored in this InMemoryStorage instance. |
||||||
|
/// </summary> |
||||||
|
public int MaxItemCount |
||||||
|
{ |
||||||
|
get { return _maxItemCount; } |
||||||
|
set |
||||||
|
{ |
||||||
|
if (_maxItemCount == value) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
_maxItemCount = value; |
||||||
|
|
||||||
|
lock (_settingMaxItemCountLocker) |
||||||
|
{ |
||||||
|
EnsureStorageBounds(value); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Clears all items stored in memory |
||||||
|
/// </summary> |
||||||
|
public void Clear() |
||||||
|
{ |
||||||
|
_inMemoryStorage.Clear(); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Clears items stored in memory based on duration passed |
||||||
|
/// </summary> |
||||||
|
/// <param name="duration">TimeSpan to identify expired items</param> |
||||||
|
public void Clear(TimeSpan duration) |
||||||
|
{ |
||||||
|
var expirationDate = DateTime.Now.Subtract(duration); |
||||||
|
|
||||||
|
var itemsToRemove = _inMemoryStorage |
||||||
|
.Where(kvp => kvp.Value.LastUpdated <= expirationDate) |
||||||
|
.Select(kvp => kvp.Key); |
||||||
|
|
||||||
|
if (itemsToRemove.Any()) |
||||||
|
{ |
||||||
|
Remove(itemsToRemove); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Remove items based on provided keys |
||||||
|
/// </summary> |
||||||
|
/// <param name="keys">identified of the in-memory storage item</param> |
||||||
|
public void Remove(IEnumerable<string> keys) |
||||||
|
{ |
||||||
|
foreach (var key in keys) |
||||||
|
{ |
||||||
|
if (string.IsNullOrWhiteSpace(key)) |
||||||
|
{ |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
_inMemoryStorage.TryRemove(key, out _); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Add new item to in-memory storage |
||||||
|
/// </summary> |
||||||
|
/// <param name="item">item to be stored</param> |
||||||
|
public void SetItem(InMemoryStorageItem<T> item) |
||||||
|
{ |
||||||
|
if (MaxItemCount == 0) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
_inMemoryStorage[item.Id] = item; |
||||||
|
|
||||||
|
// ensure max limit is maintained. trim older entries first |
||||||
|
if (_inMemoryStorage.Count > MaxItemCount) |
||||||
|
{ |
||||||
|
var itemsToRemove = _inMemoryStorage |
||||||
|
.OrderBy(kvp => kvp.Value.Created) |
||||||
|
.Take(_inMemoryStorage.Count - MaxItemCount) |
||||||
|
.Select(kvp => kvp.Key); |
||||||
|
Remove(itemsToRemove); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Get item from in-memory storage as long as it has not ex |
||||||
|
/// </summary> |
||||||
|
/// <param name="id">id of the in-memory storage item</param> |
||||||
|
/// <param name="duration">timespan denoting expiration</param> |
||||||
|
/// <returns>Valid item if not out of date or return null if out of date or item does not exist</returns> |
||||||
|
public InMemoryStorageItem<T>? GetItem(string id, TimeSpan duration) |
||||||
|
{ |
||||||
|
if (!_inMemoryStorage.TryGetValue(id, out var tempItem)) |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
var expirationDate = DateTime.Now.Subtract(duration); |
||||||
|
|
||||||
|
if (tempItem.LastUpdated > expirationDate) |
||||||
|
{ |
||||||
|
return tempItem; |
||||||
|
} |
||||||
|
|
||||||
|
_inMemoryStorage.TryRemove(id, out _); |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private void EnsureStorageBounds(int maxCount) |
||||||
|
{ |
||||||
|
if (_inMemoryStorage.Count == 0) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (maxCount == 0) |
||||||
|
{ |
||||||
|
_inMemoryStorage.Clear(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (_inMemoryStorage.Count > maxCount) |
||||||
|
{ |
||||||
|
Remove(_inMemoryStorage.Keys.Take(_inMemoryStorage.Count - maxCount)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
// Licensed to the .NET Foundation under one or more agreements. |
||||||
|
// The .NET Foundation licenses this file to you under the MIT license. |
||||||
|
// See the LICENSE file in the project root for more information. |
||||||
|
|
||||||
|
using System; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Generic InMemoryStorageItem holds items for InMemoryStorage. |
||||||
|
/// </summary> |
||||||
|
/// <typeparam name="T">Type is set by consuming cache</typeparam> |
||||||
|
public class InMemoryStorageItem<T> |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Gets the item identifier |
||||||
|
/// </summary> |
||||||
|
public string Id { get; private set; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets the item created timestamp. |
||||||
|
/// </summary> |
||||||
|
public DateTime Created { get; private set; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets the item last updated timestamp. |
||||||
|
/// </summary> |
||||||
|
public DateTime LastUpdated { get; private set; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets the item being stored. |
||||||
|
/// </summary> |
||||||
|
public T Item { get; private set; } |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Initializes a new instance of the <see cref="InMemoryStorageItem{T}"/> class. |
||||||
|
/// Constructor for InMemoryStorageItem |
||||||
|
/// </summary> |
||||||
|
/// <param name="id">uniquely identifies the item</param> |
||||||
|
/// <param name="lastUpdated">last updated timestamp</param> |
||||||
|
/// <param name="item">the item being stored</param> |
||||||
|
public InMemoryStorageItem(string id, DateTime lastUpdated, T item) |
||||||
|
{ |
||||||
|
Id = id; |
||||||
|
LastUpdated = lastUpdated; |
||||||
|
Item = item; |
||||||
|
Created = DateTime.Now; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2023 AvaloniaUI |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
@ -0,0 +1,48 @@ |
|||||||
|
<ResourceDictionary |
||||||
|
xmlns="https://github.com/avaloniaui" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||||
|
xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs"> |
||||||
|
<Design.PreviewWith> |
||||||
|
<Border Width="200" |
||||||
|
Height="200"> |
||||||
|
|
||||||
|
</Border> |
||||||
|
</Design.PreviewWith> |
||||||
|
|
||||||
|
<ControlTheme x:Key="{x:Type vendorLabs:BetterAsyncImage}" |
||||||
|
TargetType="vendorLabs:BetterAsyncImage"> |
||||||
|
<Setter Property="Background" Value="Transparent" /> |
||||||
|
<Setter Property="IsTabStop" Value="False" /> |
||||||
|
<Setter Property="Template"> |
||||||
|
<ControlTemplate> |
||||||
|
<Border Margin="0" |
||||||
|
Padding="0" |
||||||
|
ClipToBounds="True" |
||||||
|
Background="{TemplateBinding Background}" |
||||||
|
BorderBrush="{TemplateBinding BorderBrush}" |
||||||
|
BorderThickness="{TemplateBinding BorderThickness}" |
||||||
|
CornerRadius="{TemplateBinding CornerRadius}"> |
||||||
|
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> |
||||||
|
<Image Name="PART_PlaceholderImage" |
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalAlignment}" |
||||||
|
VerticalAlignment="{TemplateBinding VerticalAlignment}" |
||||||
|
Source="{TemplateBinding PlaceholderSource}" |
||||||
|
Stretch="{TemplateBinding PlaceholderStretch}"/> |
||||||
|
<Image Name="PART_Image" |
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalAlignment}" |
||||||
|
VerticalAlignment="{TemplateBinding VerticalAlignment}" |
||||||
|
Stretch="{TemplateBinding Stretch}"/> |
||||||
|
</Grid> |
||||||
|
</Border> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter> |
||||||
|
<Style Selector="^[State=Failed] /template/ Image#PART_Image"> |
||||||
|
<Setter Property="Opacity" |
||||||
|
Value="0.0" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^[State=Failed] /template/ Image#PART_PlaceholderImage"> |
||||||
|
<Setter Property="Opacity" |
||||||
|
Value="1.0" /> |
||||||
|
</Style> |
||||||
|
</ControlTheme> |
||||||
|
</ResourceDictionary> |
@ -0,0 +1,8 @@ |
|||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Models; |
||||||
|
|
||||||
|
public interface IInfinitelyScroll |
||||||
|
{ |
||||||
|
Task LoadNextPageAsync(); |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Models; |
||||||
|
|
||||||
|
public class OpenArtCustomNode |
||||||
|
{ |
||||||
|
public required string Title { get; set; } |
||||||
|
public List<string> Children { get; set; } = []; |
||||||
|
public bool IsInstalled { get; set; } |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Text.Json.Serialization; |
||||||
|
using Avalonia.Platform.Storage; |
||||||
|
using StabilityMatrix.Core.Models.Api.OpenArt; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Models; |
||||||
|
|
||||||
|
public class OpenArtMetadata |
||||||
|
{ |
||||||
|
[JsonPropertyName("sm_workflow_data")] |
||||||
|
public OpenArtSearchResult? Workflow { get; set; } |
||||||
|
|
||||||
|
[JsonIgnore] |
||||||
|
public string? FirstThumbnail => Workflow?.Thumbnails?.Select(x => x.Url).FirstOrDefault()?.ToString(); |
||||||
|
|
||||||
|
[JsonIgnore] |
||||||
|
public List<IStorageFile>? FilePath { get; set; } |
||||||
|
|
||||||
|
[JsonIgnore] |
||||||
|
public bool HasMetadata => Workflow?.Creator != null; |
||||||
|
} |
@ -1,7 +1,10 @@ |
|||||||
namespace StabilityMatrix.Avalonia.Models; |
using System.Collections.ObjectModel; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Models; |
||||||
|
|
||||||
public class PackageOutputCategory |
public class PackageOutputCategory |
||||||
{ |
{ |
||||||
|
public ObservableCollection<PackageOutputCategory> SubDirectories { get; set; } = new(); |
||||||
public required string Name { get; set; } |
public required string Name { get; set; } |
||||||
public required string Path { get; set; } |
public required string Path { get; set; } |
||||||
} |
} |
||||||
|
@ -0,0 +1,21 @@ |
|||||||
|
using System; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using StabilityMatrix.Core.Models.FileInterfaces; |
||||||
|
using StabilityMatrix.Core.Models.PackageModification; |
||||||
|
using StabilityMatrix.Core.Models.Progress; |
||||||
|
using StabilityMatrix.Core.Python; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Models.PackageSteps; |
||||||
|
|
||||||
|
public class UnpackSiteCustomizeStep(DirectoryPath venvPath) : IPackageStep |
||||||
|
{ |
||||||
|
public async Task ExecuteAsync(IProgress<ProgressReport>? progress = null) |
||||||
|
{ |
||||||
|
var sitePackages = venvPath.JoinDir(PyVenvRunner.RelativeSitePackagesPath); |
||||||
|
var file = sitePackages.JoinFile("sitecustomize.py"); |
||||||
|
file.Directory?.Create(); |
||||||
|
await Assets.PyScriptSiteCustomize.ExtractTo(file); |
||||||
|
} |
||||||
|
|
||||||
|
public string ProgressTitle => "Unpacking prerequisites..."; |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Services; |
||||||
|
|
||||||
|
public interface IModelDownloadLinkHandler |
||||||
|
{ |
||||||
|
Task StartListening(); |
||||||
|
} |
@ -0,0 +1,251 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using System.Web; |
||||||
|
using Avalonia.Controls.Notifications; |
||||||
|
using Avalonia.Threading; |
||||||
|
using MessagePipe; |
||||||
|
using Microsoft.Extensions.Logging; |
||||||
|
using StabilityMatrix.Avalonia.Helpers; |
||||||
|
using StabilityMatrix.Core.Api; |
||||||
|
using StabilityMatrix.Core.Attributes; |
||||||
|
using StabilityMatrix.Core.Extensions; |
||||||
|
using StabilityMatrix.Core.Models; |
||||||
|
using StabilityMatrix.Core.Models.Api; |
||||||
|
using StabilityMatrix.Core.Models.FileInterfaces; |
||||||
|
using StabilityMatrix.Core.Services; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Services; |
||||||
|
|
||||||
|
[Singleton(typeof(IModelDownloadLinkHandler)), Singleton(typeof(IAsyncDisposable))] |
||||||
|
public class ModelDownloadLinkHandler( |
||||||
|
IDistributedSubscriber<string, Uri> uriHandlerSubscriber, |
||||||
|
ILogger<ModelDownloadLinkHandler> logger, |
||||||
|
ICivitApi civitApi, |
||||||
|
INotificationService notificationService, |
||||||
|
ISettingsManager settingsManager, |
||||||
|
IDownloadService downloadService, |
||||||
|
ITrackedDownloadService trackedDownloadService |
||||||
|
) : IAsyncDisposable, IModelDownloadLinkHandler |
||||||
|
{ |
||||||
|
private IAsyncDisposable? uriHandlerSubscription; |
||||||
|
private const string DownloadCivitModel = "downloadCivitModel"; |
||||||
|
|
||||||
|
public async Task StartListening() |
||||||
|
{ |
||||||
|
uriHandlerSubscription = await uriHandlerSubscriber.SubscribeAsync( |
||||||
|
UriHandler.IpcKeySend, |
||||||
|
UriReceivedHandler |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
public async ValueTask DisposeAsync() |
||||||
|
{ |
||||||
|
if (uriHandlerSubscription is not null) |
||||||
|
{ |
||||||
|
await uriHandlerSubscription.DisposeAsync(); |
||||||
|
uriHandlerSubscription = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void UriReceivedHandler(Uri receivedUri) |
||||||
|
{ |
||||||
|
logger.LogDebug("ModelDownloadLinkHandler Received URI: {Uri}", receivedUri.PathAndQuery); |
||||||
|
if (!receivedUri.Host.Equals(DownloadCivitModel, StringComparison.OrdinalIgnoreCase)) |
||||||
|
return; |
||||||
|
|
||||||
|
var queryDict = HttpUtility.ParseQueryString(receivedUri.Query); |
||||||
|
var modelIdStr = queryDict["modelId"]; |
||||||
|
var modelVersionIdStr = queryDict["modelVersionId"]; |
||||||
|
var type = queryDict["type"]; |
||||||
|
var format = queryDict["format"]; |
||||||
|
var size = queryDict["size"]; |
||||||
|
var fp = queryDict["fp"]; |
||||||
|
|
||||||
|
if ( |
||||||
|
string.IsNullOrWhiteSpace(modelIdStr) |
||||||
|
|| string.IsNullOrWhiteSpace(type) |
||||||
|
|| string.IsNullOrWhiteSpace(format) |
||||||
|
|| !int.TryParse(modelIdStr, out var modelId) |
||||||
|
|| !Enum.TryParse<CivitFileType>(type, out var civitFileType) |
||||||
|
|| !Enum.TryParse<CivitModelFormat>(format, out var civitFormat) |
||||||
|
) |
||||||
|
{ |
||||||
|
logger.LogError("ModelDownloadLinkHandler: Invalid query parameters"); |
||||||
|
|
||||||
|
Dispatcher.UIThread.Post( |
||||||
|
() => |
||||||
|
notificationService.Show( |
||||||
|
new Notification( |
||||||
|
"Invalid Download Link", |
||||||
|
"The download link is invalid", |
||||||
|
NotificationType.Error |
||||||
|
) |
||||||
|
) |
||||||
|
); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Dispatcher.UIThread.Post( |
||||||
|
() => |
||||||
|
notificationService.Show( |
||||||
|
"Link Received", |
||||||
|
"Successfully received download link", |
||||||
|
NotificationType.Warning |
||||||
|
) |
||||||
|
); |
||||||
|
|
||||||
|
var modelTask = civitApi.GetModelById(modelId); |
||||||
|
modelTask.Wait(); |
||||||
|
var model = modelTask.Result; |
||||||
|
|
||||||
|
var useModelVersion = !string.IsNullOrWhiteSpace(modelVersionIdStr); |
||||||
|
var modelVersionId = useModelVersion ? int.Parse(modelVersionIdStr) : 0; |
||||||
|
|
||||||
|
var modelVersion = useModelVersion |
||||||
|
? model.ModelVersions?.FirstOrDefault(x => x.Id == modelVersionId) |
||||||
|
: model.ModelVersions?.FirstOrDefault(); |
||||||
|
|
||||||
|
if (modelVersion is null) |
||||||
|
{ |
||||||
|
logger.LogError("ModelDownloadLinkHandler: Model version not found"); |
||||||
|
Dispatcher.UIThread.Post( |
||||||
|
() => |
||||||
|
notificationService.Show( |
||||||
|
new Notification( |
||||||
|
"Model has no versions available", |
||||||
|
"This model has no versions available for download", |
||||||
|
NotificationType.Error |
||||||
|
) |
||||||
|
) |
||||||
|
); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var possibleFiles = modelVersion.Files?.Where( |
||||||
|
x => x.Type == civitFileType && x.Metadata.Format == civitFormat |
||||||
|
); |
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(fp) && Enum.TryParse<CivitModelFpType>(fp, out var fpType)) |
||||||
|
{ |
||||||
|
possibleFiles = possibleFiles?.Where(x => x.Metadata.Fp == fpType); |
||||||
|
} |
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(size) && Enum.TryParse<CivitModelSize>(size, out var modelSize)) |
||||||
|
{ |
||||||
|
possibleFiles = possibleFiles?.Where(x => x.Metadata.Size == modelSize); |
||||||
|
} |
||||||
|
|
||||||
|
possibleFiles = possibleFiles?.ToList(); |
||||||
|
|
||||||
|
if (possibleFiles is null) |
||||||
|
{ |
||||||
|
Dispatcher.UIThread.Post( |
||||||
|
() => |
||||||
|
notificationService.Show( |
||||||
|
new Notification( |
||||||
|
"Model has no files available", |
||||||
|
"This model has no files available for download", |
||||||
|
NotificationType.Error |
||||||
|
) |
||||||
|
) |
||||||
|
); |
||||||
|
logger.LogError("ModelDownloadLinkHandler: Model file not found"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var selectedFile = possibleFiles.FirstOrDefault() ?? modelVersion.Files?.FirstOrDefault(); |
||||||
|
|
||||||
|
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); |
||||||
|
var downloadDirectory = rootModelsDirectory.JoinDir( |
||||||
|
selectedFile.Type == CivitFileType.VAE |
||||||
|
? SharedFolderType.VAE.GetStringValue() |
||||||
|
: model.Type.ConvertTo<SharedFolderType>().GetStringValue() |
||||||
|
); |
||||||
|
|
||||||
|
downloadDirectory.Create(); |
||||||
|
var downloadPath = downloadDirectory.JoinFile(selectedFile.Name); |
||||||
|
|
||||||
|
// Create tracked download |
||||||
|
var download = trackedDownloadService.NewDownload(selectedFile.DownloadUrl, downloadPath); |
||||||
|
|
||||||
|
// Download model info and preview first |
||||||
|
var saveCmInfoTask = SaveCmInfo(model, modelVersion, selectedFile, downloadDirectory); |
||||||
|
var savePreviewImageTask = SavePreviewImage(modelVersion, downloadPath); |
||||||
|
|
||||||
|
Task.WaitAll([saveCmInfoTask, savePreviewImageTask]); |
||||||
|
|
||||||
|
var cmInfoPath = saveCmInfoTask.Result; |
||||||
|
var previewImagePath = savePreviewImageTask.Result; |
||||||
|
|
||||||
|
// Add hash info |
||||||
|
download.ExpectedHashSha256 = selectedFile.Hashes.SHA256; |
||||||
|
|
||||||
|
// Add files to cleanup list |
||||||
|
download.ExtraCleanupFileNames.Add(cmInfoPath); |
||||||
|
if (previewImagePath is not null) |
||||||
|
{ |
||||||
|
download.ExtraCleanupFileNames.Add(previewImagePath); |
||||||
|
} |
||||||
|
|
||||||
|
// Add hash context action |
||||||
|
download.ContextAction = CivitPostDownloadContextAction.FromCivitFile(selectedFile); |
||||||
|
|
||||||
|
download.Start(); |
||||||
|
|
||||||
|
Dispatcher.UIThread.Post( |
||||||
|
() => notificationService.Show("Download Started", $"Downloading {selectedFile.Name}") |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
private static async Task<FilePath> SaveCmInfo( |
||||||
|
CivitModel model, |
||||||
|
CivitModelVersion modelVersion, |
||||||
|
CivitFile modelFile, |
||||||
|
DirectoryPath downloadDirectory |
||||||
|
) |
||||||
|
{ |
||||||
|
var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Name); |
||||||
|
var modelInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTime.UtcNow); |
||||||
|
|
||||||
|
await modelInfo.SaveJsonToDirectory(downloadDirectory, modelFileName); |
||||||
|
|
||||||
|
var jsonName = $"{modelFileName}.cm-info.json"; |
||||||
|
return downloadDirectory.JoinFile(jsonName); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Saves the preview image to the same directory as the model file |
||||||
|
/// </summary> |
||||||
|
/// <param name="modelVersion"></param> |
||||||
|
/// <param name="modelFilePath"></param> |
||||||
|
/// <returns>The file path of the saved preview image</returns> |
||||||
|
private async Task<FilePath?> SavePreviewImage(CivitModelVersion modelVersion, FilePath modelFilePath) |
||||||
|
{ |
||||||
|
// Skip if model has no images |
||||||
|
if (modelVersion.Images == null || modelVersion.Images.Count == 0) |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
var image = modelVersion.Images.FirstOrDefault(x => x.Type == "image"); |
||||||
|
if (image is null) |
||||||
|
return null; |
||||||
|
|
||||||
|
var imageExtension = Path.GetExtension(image.Url).TrimStart('.'); |
||||||
|
if (imageExtension is "jpg" or "jpeg" or "png") |
||||||
|
{ |
||||||
|
var imageDownloadPath = modelFilePath.Directory!.JoinFile( |
||||||
|
$"{modelFilePath.NameWithoutExtension}.preview.{imageExtension}" |
||||||
|
); |
||||||
|
|
||||||
|
var imageTask = downloadService.DownloadToFileAsync(image.Url, imageDownloadPath); |
||||||
|
await notificationService.TryAsync(imageTask, "Could not download preview image"); |
||||||
|
|
||||||
|
return imageDownloadPath; |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,153 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Immutable; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using Avalonia.Controls.Notifications; |
||||||
|
using CommunityToolkit.Mvvm.ComponentModel; |
||||||
|
using Microsoft.Extensions.Logging; |
||||||
|
using StabilityMatrix.Avalonia.Models; |
||||||
|
using StabilityMatrix.Avalonia.ViewModels; |
||||||
|
using StabilityMatrix.Core.Attributes; |
||||||
|
using StabilityMatrix.Core.Extensions; |
||||||
|
using StabilityMatrix.Core.Helper.Factory; |
||||||
|
using StabilityMatrix.Core.Models; |
||||||
|
using StabilityMatrix.Core.Models.FileInterfaces; |
||||||
|
using StabilityMatrix.Core.Models.Packages; |
||||||
|
using StabilityMatrix.Core.Python; |
||||||
|
using StabilityMatrix.Core.Services; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Services; |
||||||
|
|
||||||
|
[Singleton] |
||||||
|
public partial class RunningPackageService( |
||||||
|
ILogger<RunningPackageService> logger, |
||||||
|
IPackageFactory packageFactory, |
||||||
|
INotificationService notificationService, |
||||||
|
ISettingsManager settingsManager, |
||||||
|
IPyRunner pyRunner |
||||||
|
) : ObservableObject |
||||||
|
{ |
||||||
|
// 🤔 what if we put the ConsoleViewModel inside the BasePackage? 🤔 |
||||||
|
[ObservableProperty] |
||||||
|
private ObservableDictionary<Guid, RunningPackageViewModel> runningPackages = []; |
||||||
|
|
||||||
|
public async Task<PackagePair?> StartPackage(InstalledPackage installedPackage, string? command = null) |
||||||
|
{ |
||||||
|
var activeInstallName = installedPackage.PackageName; |
||||||
|
var basePackage = string.IsNullOrWhiteSpace(activeInstallName) |
||||||
|
? null |
||||||
|
: packageFactory.GetNewBasePackage(installedPackage); |
||||||
|
|
||||||
|
if (basePackage == null) |
||||||
|
{ |
||||||
|
logger.LogWarning( |
||||||
|
"During launch, package name '{PackageName}' did not match a definition", |
||||||
|
activeInstallName |
||||||
|
); |
||||||
|
|
||||||
|
notificationService.Show( |
||||||
|
new Notification( |
||||||
|
"Package name invalid", |
||||||
|
"Install package name did not match a definition. Please reinstall and let us know about this issue.", |
||||||
|
NotificationType.Error |
||||||
|
) |
||||||
|
); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
// If this is the first launch (LaunchArgs is null), |
||||||
|
// load and save a launch options dialog vm |
||||||
|
// so that dynamic initial values are saved. |
||||||
|
if (installedPackage.LaunchArgs == null) |
||||||
|
{ |
||||||
|
var definitions = basePackage.LaunchOptions; |
||||||
|
// Create config cards and save them |
||||||
|
var cards = LaunchOptionCard |
||||||
|
.FromDefinitions(definitions, Array.Empty<LaunchOption>()) |
||||||
|
.ToImmutableArray(); |
||||||
|
|
||||||
|
var args = cards.SelectMany(c => c.Options).ToList(); |
||||||
|
|
||||||
|
logger.LogDebug( |
||||||
|
"Setting initial launch args: {Args}", |
||||||
|
string.Join(", ", args.Select(o => o.ToArgString()?.ToRepr())) |
||||||
|
); |
||||||
|
|
||||||
|
settingsManager.SaveLaunchArgs(installedPackage.Id, args); |
||||||
|
} |
||||||
|
|
||||||
|
if (basePackage is not StableSwarm) |
||||||
|
{ |
||||||
|
await pyRunner.Initialize(); |
||||||
|
} |
||||||
|
|
||||||
|
// Get path from package |
||||||
|
var packagePath = new DirectoryPath(settingsManager.LibraryDir, installedPackage.LibraryPath!); |
||||||
|
|
||||||
|
if (basePackage is not StableSwarm) |
||||||
|
{ |
||||||
|
// Unpack sitecustomize.py to venv |
||||||
|
await UnpackSiteCustomize(packagePath.JoinDir("venv")); |
||||||
|
} |
||||||
|
|
||||||
|
// Clear console and start update processing |
||||||
|
var console = new ConsoleViewModel(); |
||||||
|
console.StartUpdates(); |
||||||
|
|
||||||
|
// Update shared folder links (in case library paths changed) |
||||||
|
await basePackage.UpdateModelFolders( |
||||||
|
packagePath, |
||||||
|
installedPackage.PreferredSharedFolderMethod ?? basePackage.RecommendedSharedFolderMethod |
||||||
|
); |
||||||
|
|
||||||
|
if (installedPackage.UseSharedOutputFolder) |
||||||
|
{ |
||||||
|
await basePackage.SetupOutputFolderLinks(installedPackage.FullPath!); |
||||||
|
} |
||||||
|
|
||||||
|
// Load user launch args from settings and convert to string |
||||||
|
var userArgs = installedPackage.LaunchArgs ?? []; |
||||||
|
var userArgsString = string.Join(" ", userArgs.Select(opt => opt.ToArgString())); |
||||||
|
|
||||||
|
// Join with extras, if any |
||||||
|
userArgsString = string.Join(" ", userArgsString, basePackage.ExtraLaunchArguments); |
||||||
|
|
||||||
|
// Use input command if provided, otherwise use package launch command |
||||||
|
command ??= basePackage.LaunchCommand; |
||||||
|
|
||||||
|
await basePackage.RunPackage(packagePath, command, userArgsString, o => console.Post(o)); |
||||||
|
var runningPackage = new PackagePair(installedPackage, basePackage); |
||||||
|
|
||||||
|
var viewModel = new RunningPackageViewModel( |
||||||
|
settingsManager, |
||||||
|
notificationService, |
||||||
|
this, |
||||||
|
runningPackage, |
||||||
|
console |
||||||
|
); |
||||||
|
RunningPackages.Add(runningPackage.InstalledPackage.Id, viewModel); |
||||||
|
|
||||||
|
return runningPackage; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task StopPackage(Guid id) |
||||||
|
{ |
||||||
|
if (RunningPackages.TryGetValue(id, out var vm)) |
||||||
|
{ |
||||||
|
var runningPackage = vm.RunningPackage; |
||||||
|
await runningPackage.BasePackage.WaitForShutdown(); |
||||||
|
RunningPackages.Remove(id); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public RunningPackageViewModel? GetRunningPackageViewModel(Guid id) => |
||||||
|
RunningPackages.TryGetValue(id, out var vm) ? vm : null; |
||||||
|
|
||||||
|
private static async Task UnpackSiteCustomize(DirectoryPath venvPath) |
||||||
|
{ |
||||||
|
var sitePackages = venvPath.JoinDir(PyVenvRunner.RelativeSitePackagesPath); |
||||||
|
var file = sitePackages.JoinFile("sitecustomize.py"); |
||||||
|
file.Directory?.Create(); |
||||||
|
await Assets.PyScriptSiteCustomize.ExtractTo(file, true); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,403 @@ |
|||||||
|
<Styles xmlns="https://github.com/avaloniaui" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||||
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> |
||||||
|
<Design.PreviewWith> |
||||||
|
<Border Padding="20"> |
||||||
|
<ui:CommandBar |
||||||
|
Margin="8" |
||||||
|
VerticalAlignment="Center" |
||||||
|
VerticalContentAlignment="Center" |
||||||
|
HorizontalAlignment="Left" |
||||||
|
HorizontalContentAlignment="Left" |
||||||
|
DefaultLabelPosition="Right"> |
||||||
|
<ui:CommandBar.PrimaryCommands> |
||||||
|
<ui:CommandBarButton Classes="success" Label="Success Button" Margin="8" |
||||||
|
HorizontalAlignment="Center" /> |
||||||
|
<ui:CommandBarButton Classes="accent" Label="FA Accent Button" Margin="8" |
||||||
|
HorizontalAlignment="Center" /> |
||||||
|
<ui:CommandBarButton Classes="systemaccent" Label="System Accent Button" Margin="8" |
||||||
|
HorizontalAlignment="Center" /> |
||||||
|
<ui:CommandBarButton Classes="danger" Label="Danger Button" Margin="8" HorizontalAlignment="Center" /> |
||||||
|
<ui:CommandBarButton Classes="info" Label="Info Button" Margin="8" HorizontalAlignment="Center" /> |
||||||
|
<ui:CommandBarButton Classes="transparent-info" Label="Semi-Transparent Info Button" Margin="8" |
||||||
|
HorizontalAlignment="Center" /> |
||||||
|
<ui:CommandBarButton Classes="transparent" Label="Transparent Button" Margin="8" |
||||||
|
HorizontalAlignment="Center" /> |
||||||
|
<ui:CommandBarButton Classes="transparent-full" Label="Transparent Button" Margin="8" |
||||||
|
HorizontalAlignment="Center" /> |
||||||
|
<ui:CommandBarButton Label="Disabled Button" Margin="8" IsEnabled="False" |
||||||
|
HorizontalAlignment="Center" /> |
||||||
|
</ui:CommandBar.PrimaryCommands> |
||||||
|
</ui:CommandBar> |
||||||
|
</Border> |
||||||
|
</Design.PreviewWith> |
||||||
|
|
||||||
|
<!-- Success --> |
||||||
|
<Style Selector="ui|CommandBarButton.success"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeGreenColor}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeGreenColor}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^ /template/ TextBlock#TextLabel"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pointerover"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeDarkGreenColor}"/> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkGreenColor}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="Green" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pressed"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeDarkDarkGreenColor}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkDarkGreenColor}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:disabled"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<!-- Danger --> |
||||||
|
<Style Selector="ui|CommandBarButton.danger"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeRedColor}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeRedColor}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pointerover"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeDarkRedColor}"/> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkRedColor}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pressed"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeDarkDarkRedColor}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkDarkRedColor}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:disabled"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<!-- Info --> |
||||||
|
<Style Selector="ui|CommandBarButton.info"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeLightBlueColor}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeLightBlueColor}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pointerover"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeBlueColor}"/> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBlueColor}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pressed"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColor}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkBlueColor}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:disabled"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<!--Accent Button--> |
||||||
|
<Style Selector="ui|CommandBarButton.accent"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" /> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pointerover"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPointerOver}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pressed"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPressed}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:disabled"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<!-- SystemAccent --> |
||||||
|
<Style Selector="ui|CommandBarButton.systemaccent"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pointerover"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemAccentColorDark1}"/> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark1}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pressed"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemAccentColorDark2}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark2}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:disabled"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<!-- Transparent --> |
||||||
|
<Style Selector="ui|CommandBarButton.transparent"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pointerover"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pressed"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:disabled"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<!-- Semi-Transparent Info --> |
||||||
|
<Style Selector="ui|CommandBarButton.transparent-info"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeLightBlueColorTransparent}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeLightBlueColorTransparent}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pointerover"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeBlueColorTransparent}"/> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBlueColorTransparent}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pressed"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColorTransparent}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkBlueColorTransparent}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:disabled"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<!-- Transparent red --> |
||||||
|
<Style Selector="ui|CommandBarButton.transparent-red"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pointerover"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeCoralRedColor}"/> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeCoralRedColor}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pressed"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ThemeDarkCoralRedColor}"/> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkCoralRedColor}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:disabled"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<!-- Full Transparent --> |
||||||
|
<Style Selector="ui|CommandBarButton.transparent-full"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pointerover"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:pressed"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
|
||||||
|
<Style Selector="^:disabled"> |
||||||
|
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||||
|
</Style> |
||||||
|
<Style Selector="^"> |
||||||
|
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
</Style> |
||||||
|
</Styles> |
@ -0,0 +1,185 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Collections.ObjectModel; |
||||||
|
using System.ComponentModel; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using AsyncAwaitBestPractices; |
||||||
|
using Avalonia.Controls; |
||||||
|
using CommunityToolkit.Mvvm.ComponentModel; |
||||||
|
using StabilityMatrix.Avalonia.Models; |
||||||
|
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||||
|
using StabilityMatrix.Avalonia.Views.Dialogs; |
||||||
|
using StabilityMatrix.Core.Attributes; |
||||||
|
using StabilityMatrix.Core.Helper; |
||||||
|
using StabilityMatrix.Core.Helper.Factory; |
||||||
|
using StabilityMatrix.Core.Models; |
||||||
|
using StabilityMatrix.Core.Models.Api.OpenArt; |
||||||
|
using StabilityMatrix.Core.Models.Packages.Extensions; |
||||||
|
using StabilityMatrix.Core.Services; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; |
||||||
|
|
||||||
|
[View(typeof(OpenArtWorkflowDialog))] |
||||||
|
[ManagedService] |
||||||
|
[Transient] |
||||||
|
public partial class OpenArtWorkflowViewModel( |
||||||
|
ISettingsManager settingsManager, |
||||||
|
IPackageFactory packageFactory |
||||||
|
) : ContentDialogViewModelBase |
||||||
|
{ |
||||||
|
public required OpenArtSearchResult Workflow { get; init; } |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private ObservableCollection<OpenArtCustomNode> customNodes = []; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private string prunedDescription = string.Empty; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private bool installRequiredNodes = true; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private InstalledPackage? selectedPackage; |
||||||
|
|
||||||
|
public PackagePair? SelectedPackagePair => |
||||||
|
SelectedPackage is { } package ? packageFactory.GetPackagePair(package) : null; |
||||||
|
|
||||||
|
public List<InstalledPackage> AvailablePackages => |
||||||
|
settingsManager |
||||||
|
.Settings.InstalledPackages.Where(package => package.PackageName == "ComfyUI") |
||||||
|
.ToList(); |
||||||
|
|
||||||
|
public List<PackageExtension> MissingNodes { get; } = []; |
||||||
|
|
||||||
|
public override async Task OnLoadedAsync() |
||||||
|
{ |
||||||
|
if (Design.IsDesignMode) |
||||||
|
return; |
||||||
|
|
||||||
|
if (settingsManager.Settings.PreferredWorkflowPackage is { } preferredPackage) |
||||||
|
{ |
||||||
|
SelectedPackage = preferredPackage; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
SelectedPackage = AvailablePackages.FirstOrDefault(); |
||||||
|
} |
||||||
|
|
||||||
|
if (SelectedPackage == null) |
||||||
|
{ |
||||||
|
InstallRequiredNodes = false; |
||||||
|
} |
||||||
|
|
||||||
|
CustomNodes = new ObservableCollection<OpenArtCustomNode>( |
||||||
|
await ParseNodes(Workflow.NodesIndex.ToList()) |
||||||
|
); |
||||||
|
PrunedDescription = Utilities.RemoveHtml(Workflow.Description); |
||||||
|
} |
||||||
|
|
||||||
|
partial void OnSelectedPackageChanged(InstalledPackage? oldValue, InstalledPackage? newValue) |
||||||
|
{ |
||||||
|
if (oldValue is null) |
||||||
|
return; |
||||||
|
|
||||||
|
settingsManager.Transaction(settings => |
||||||
|
{ |
||||||
|
settings.PreferredWorkflowPackage = newValue; |
||||||
|
}); |
||||||
|
|
||||||
|
OnLoadedAsync().SafeFireAndForget(); |
||||||
|
} |
||||||
|
|
||||||
|
[Localizable(false)] |
||||||
|
private async Task<List<OpenArtCustomNode>> ParseNodes(List<string> nodes) |
||||||
|
{ |
||||||
|
var indexOfFirstDot = nodes.IndexOf("."); |
||||||
|
if (indexOfFirstDot != -1) |
||||||
|
{ |
||||||
|
nodes = nodes[(indexOfFirstDot + 1)..]; |
||||||
|
} |
||||||
|
|
||||||
|
var installedNodesNames = new HashSet<string>(); |
||||||
|
var nameToManifestNodes = new Dictionary<string, PackageExtension>(); |
||||||
|
|
||||||
|
var packagePair = SelectedPackagePair; |
||||||
|
|
||||||
|
if (packagePair?.BasePackage.ExtensionManager is { } extensionManager) |
||||||
|
{ |
||||||
|
var installedNodes = ( |
||||||
|
await extensionManager.GetInstalledExtensionsLiteAsync(packagePair.InstalledPackage) |
||||||
|
).ToList(); |
||||||
|
|
||||||
|
var manifestExtensionsMap = await extensionManager.GetManifestExtensionsMapAsync( |
||||||
|
extensionManager.GetManifests(packagePair.InstalledPackage) |
||||||
|
); |
||||||
|
|
||||||
|
// Add manifestExtensions definition to installedNodes if matching git repository url |
||||||
|
installedNodes = installedNodes |
||||||
|
.Select(installedNode => |
||||||
|
{ |
||||||
|
if ( |
||||||
|
installedNode.GitRepositoryUrl is not null |
||||||
|
&& manifestExtensionsMap.TryGetValue( |
||||||
|
installedNode.GitRepositoryUrl, |
||||||
|
out var manifestExtension |
||||||
|
) |
||||||
|
) |
||||||
|
{ |
||||||
|
installedNode = installedNode with { Definition = manifestExtension }; |
||||||
|
} |
||||||
|
|
||||||
|
return installedNode; |
||||||
|
}) |
||||||
|
.ToList(); |
||||||
|
|
||||||
|
// There may be duplicate titles, deduplicate by using the first one |
||||||
|
nameToManifestNodes = manifestExtensionsMap |
||||||
|
.GroupBy(x => x.Value.Title) |
||||||
|
.ToDictionary(x => x.Key, x => x.First().Value); |
||||||
|
|
||||||
|
installedNodesNames = installedNodes.Select(x => x.Title).ToHashSet(); |
||||||
|
} |
||||||
|
|
||||||
|
var sections = new List<OpenArtCustomNode>(); |
||||||
|
OpenArtCustomNode? currentSection = null; |
||||||
|
|
||||||
|
foreach (var node in nodes) |
||||||
|
{ |
||||||
|
if (node is "." or ",") |
||||||
|
{ |
||||||
|
currentSection = null; // End of the current section |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (currentSection == null) |
||||||
|
{ |
||||||
|
currentSection = new OpenArtCustomNode |
||||||
|
{ |
||||||
|
Title = node, |
||||||
|
IsInstalled = installedNodesNames.Contains(node) |
||||||
|
}; |
||||||
|
|
||||||
|
// Add missing nodes to the list |
||||||
|
if ( |
||||||
|
!currentSection.IsInstalled && nameToManifestNodes.TryGetValue(node, out var manifestNode) |
||||||
|
) |
||||||
|
{ |
||||||
|
MissingNodes.Add(manifestNode); |
||||||
|
} |
||||||
|
|
||||||
|
sections.Add(currentSection); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
currentSection.Children.Add(node); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (sections.FirstOrDefault(x => x.Title == "ComfyUI") != null) |
||||||
|
{ |
||||||
|
sections = sections.Where(x => x.Title != "ComfyUI").ToList(); |
||||||
|
} |
||||||
|
|
||||||
|
return sections; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,100 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.ComponentModel.DataAnnotations; |
||||||
|
using System.Linq; |
||||||
|
using CommunityToolkit.Mvvm.ComponentModel; |
||||||
|
using KGySoft.CoreLibraries; |
||||||
|
using StabilityMatrix.Avalonia.Controls; |
||||||
|
using StabilityMatrix.Avalonia.Models.Inference; |
||||||
|
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||||
|
using StabilityMatrix.Core.Attributes; |
||||||
|
using StabilityMatrix.Core.Models.Api.Comfy.Nodes; |
||||||
|
using StabilityMatrix.Core.Models.Inference; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.ViewModels.Inference; |
||||||
|
|
||||||
|
[Transient] |
||||||
|
[ManagedService] |
||||||
|
[View(typeof(LayerDiffuseCard))] |
||||||
|
public partial class LayerDiffuseCardViewModel : LoadableViewModelBase, IComfyStep |
||||||
|
{ |
||||||
|
public const string ModuleKey = "LayerDiffuse"; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private LayerDiffuseMode selectedMode = LayerDiffuseMode.None; |
||||||
|
|
||||||
|
public IEnumerable<LayerDiffuseMode> AvailableModes => Enum<LayerDiffuseMode>.GetValues(); |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[NotifyDataErrorInfo] |
||||||
|
[Required] |
||||||
|
[Range(-1d, 3d)] |
||||||
|
private double weight = 1; |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public void ApplyStep(ModuleApplyStepEventArgs e) |
||||||
|
{ |
||||||
|
if (SelectedMode == LayerDiffuseMode.None) |
||||||
|
return; |
||||||
|
|
||||||
|
var sdType = SelectedMode switch |
||||||
|
{ |
||||||
|
LayerDiffuseMode.GenerateForegroundWithTransparencySD15 => "SD15", |
||||||
|
LayerDiffuseMode.GenerateForegroundWithTransparencySDXL => "SDXL", |
||||||
|
LayerDiffuseMode.None => throw new ArgumentOutOfRangeException(), |
||||||
|
_ => throw new ArgumentOutOfRangeException() |
||||||
|
}; |
||||||
|
|
||||||
|
// Choose config based on mode |
||||||
|
var config = SelectedMode switch |
||||||
|
{ |
||||||
|
LayerDiffuseMode.GenerateForegroundWithTransparencySD15 |
||||||
|
=> "SD15, Attention Injection, attn_sharing", |
||||||
|
LayerDiffuseMode.GenerateForegroundWithTransparencySDXL => "SDXL, Conv Injection", |
||||||
|
LayerDiffuseMode.None => throw new ArgumentOutOfRangeException(), |
||||||
|
_ => throw new ArgumentOutOfRangeException() |
||||||
|
}; |
||||||
|
|
||||||
|
foreach (var modelConnections in e.Temp.Models.Values) |
||||||
|
{ |
||||||
|
var layerDiffuseApply = e.Nodes.AddTypedNode( |
||||||
|
new ComfyNodeBuilder.LayeredDiffusionApply |
||||||
|
{ |
||||||
|
Name = e.Nodes.GetUniqueName($"LayerDiffuseApply_{modelConnections.Name}"), |
||||||
|
Model = modelConnections.Model, |
||||||
|
Config = config, |
||||||
|
Weight = Weight, |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
modelConnections.Model = layerDiffuseApply.Output; |
||||||
|
} |
||||||
|
|
||||||
|
// Add pre output action |
||||||
|
e.PreOutputActions.Add(applyArgs => |
||||||
|
{ |
||||||
|
// Use last latent for decode |
||||||
|
var latent = |
||||||
|
applyArgs.Builder.Connections.LastPrimaryLatent |
||||||
|
?? throw new InvalidOperationException("Connections.LastPrimaryLatent not set"); |
||||||
|
|
||||||
|
// Convert primary to image if not already |
||||||
|
var primaryImage = applyArgs.Builder.GetPrimaryAsImage(); |
||||||
|
applyArgs.Builder.Connections.Primary = primaryImage; |
||||||
|
|
||||||
|
// Add a Layer Diffuse Decode |
||||||
|
var decode = applyArgs.Nodes.AddTypedNode( |
||||||
|
new ComfyNodeBuilder.LayeredDiffusionDecodeRgba |
||||||
|
{ |
||||||
|
Name = applyArgs.Nodes.GetUniqueName("LayerDiffuseDecode"), |
||||||
|
Samples = latent, |
||||||
|
Images = primaryImage, |
||||||
|
SdVersion = sdType |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
// Set primary to decode output |
||||||
|
applyArgs.Builder.Connections.Primary = decode.Output; |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
using StabilityMatrix.Avalonia.Models.Inference; |
||||||
|
using StabilityMatrix.Avalonia.Services; |
||||||
|
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||||
|
using StabilityMatrix.Core.Attributes; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; |
||||||
|
|
||||||
|
[ManagedService] |
||||||
|
[Transient] |
||||||
|
public class LayerDiffuseModule : ModuleBase |
||||||
|
{ |
||||||
|
/// <inheritdoc /> |
||||||
|
public LayerDiffuseModule(ServiceManager<ViewModelBase> vmFactory) |
||||||
|
: base(vmFactory) |
||||||
|
{ |
||||||
|
Title = "Layer Diffuse"; |
||||||
|
AddCards(vmFactory.Get<LayerDiffuseCardViewModel>()); |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
protected override void OnApplyStep(ModuleApplyStepEventArgs e) |
||||||
|
{ |
||||||
|
var card = GetCard<LayerDiffuseCardViewModel>(); |
||||||
|
card.ApplyStep(e); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,173 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Text.Json; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using AsyncAwaitBestPractices; |
||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.Platform.Storage; |
||||||
|
using CommunityToolkit.Mvvm.ComponentModel; |
||||||
|
using CommunityToolkit.Mvvm.Input; |
||||||
|
using DynamicData; |
||||||
|
using DynamicData.Binding; |
||||||
|
using FluentAvalonia.UI.Controls; |
||||||
|
using StabilityMatrix.Avalonia.Controls; |
||||||
|
using StabilityMatrix.Avalonia.Languages; |
||||||
|
using StabilityMatrix.Avalonia.Models; |
||||||
|
using StabilityMatrix.Avalonia.Services; |
||||||
|
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||||
|
using StabilityMatrix.Avalonia.Views; |
||||||
|
using StabilityMatrix.Core.Attributes; |
||||||
|
using StabilityMatrix.Core.Helper; |
||||||
|
using StabilityMatrix.Core.Models.Api.OpenArt; |
||||||
|
using StabilityMatrix.Core.Processes; |
||||||
|
using StabilityMatrix.Core.Services; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.ViewModels; |
||||||
|
|
||||||
|
[View(typeof(InstalledWorkflowsPage))] |
||||||
|
[Singleton] |
||||||
|
public partial class InstalledWorkflowsViewModel( |
||||||
|
ISettingsManager settingsManager, |
||||||
|
INotificationService notificationService |
||||||
|
) : TabViewModelBase, IDisposable |
||||||
|
{ |
||||||
|
public override string Header => Resources.TabLabel_InstalledWorkflows; |
||||||
|
|
||||||
|
private readonly SourceCache<OpenArtMetadata, string> workflowsCache = |
||||||
|
new(x => x.Workflow?.Id ?? Guid.NewGuid().ToString()); |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private IObservableCollection<OpenArtMetadata> displayedWorkflows = |
||||||
|
new ObservableCollectionExtended<OpenArtMetadata>(); |
||||||
|
|
||||||
|
protected override async Task OnInitialLoadedAsync() |
||||||
|
{ |
||||||
|
await base.OnInitialLoadedAsync(); |
||||||
|
|
||||||
|
workflowsCache.Connect().DeferUntilLoaded().Bind(DisplayedWorkflows).Subscribe(); |
||||||
|
|
||||||
|
if (Design.IsDesignMode) |
||||||
|
return; |
||||||
|
|
||||||
|
await LoadInstalledWorkflowsAsync(); |
||||||
|
EventManager.Instance.WorkflowInstalled += OnWorkflowInstalled; |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private async Task LoadInstalledWorkflowsAsync() |
||||||
|
{ |
||||||
|
workflowsCache.Clear(); |
||||||
|
|
||||||
|
if (!Directory.Exists(settingsManager.WorkflowDirectory)) |
||||||
|
{ |
||||||
|
Directory.CreateDirectory(settingsManager.WorkflowDirectory); |
||||||
|
} |
||||||
|
|
||||||
|
foreach ( |
||||||
|
var workflowPath in Directory.EnumerateFiles( |
||||||
|
settingsManager.WorkflowDirectory, |
||||||
|
"*.json", |
||||||
|
SearchOption.AllDirectories |
||||||
|
) |
||||||
|
) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
var json = await File.ReadAllTextAsync(workflowPath); |
||||||
|
var metadata = JsonSerializer.Deserialize<OpenArtMetadata>(json); |
||||||
|
|
||||||
|
if (metadata?.Workflow == null) |
||||||
|
{ |
||||||
|
metadata = new OpenArtMetadata |
||||||
|
{ |
||||||
|
Workflow = new OpenArtSearchResult |
||||||
|
{ |
||||||
|
Id = Guid.NewGuid().ToString(), |
||||||
|
Name = Path.GetFileNameWithoutExtension(workflowPath) |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
metadata.FilePath = [await App.StorageProvider.TryGetFileFromPathAsync(workflowPath)]; |
||||||
|
workflowsCache.AddOrUpdate(metadata); |
||||||
|
} |
||||||
|
catch (Exception e) |
||||||
|
{ |
||||||
|
Console.WriteLine(e); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private async Task OpenInExplorer(OpenArtMetadata metadata) |
||||||
|
{ |
||||||
|
if (metadata.FilePath == null) |
||||||
|
return; |
||||||
|
|
||||||
|
var path = metadata.FilePath.FirstOrDefault()?.Path.ToString(); |
||||||
|
if (string.IsNullOrWhiteSpace(path)) |
||||||
|
return; |
||||||
|
|
||||||
|
await ProcessRunner.OpenFileBrowser(path); |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private void OpenOnOpenArt(OpenArtMetadata metadata) |
||||||
|
{ |
||||||
|
if (metadata.Workflow == null) |
||||||
|
return; |
||||||
|
|
||||||
|
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{metadata.Workflow.Id}"); |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private async Task DeleteAsync(OpenArtMetadata metadata) |
||||||
|
{ |
||||||
|
var confirmationDialog = new BetterContentDialog |
||||||
|
{ |
||||||
|
Title = Resources.Label_AreYouSure, |
||||||
|
Content = Resources.Label_ActionCannotBeUndone, |
||||||
|
PrimaryButtonText = Resources.Action_Delete, |
||||||
|
SecondaryButtonText = Resources.Action_Cancel, |
||||||
|
DefaultButton = ContentDialogButton.Primary, |
||||||
|
IsSecondaryButtonEnabled = true, |
||||||
|
}; |
||||||
|
var dialogResult = await confirmationDialog.ShowAsync(); |
||||||
|
if (dialogResult != ContentDialogResult.Primary) |
||||||
|
return; |
||||||
|
|
||||||
|
await using var delay = new MinimumDelay(200, 500); |
||||||
|
|
||||||
|
var path = metadata?.FilePath?.FirstOrDefault()?.Path.ToString().Replace("file:///", ""); |
||||||
|
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) |
||||||
|
{ |
||||||
|
await notificationService.TryAsync( |
||||||
|
Task.Run(() => File.Delete(path)), |
||||||
|
message: "Error deleting workflow" |
||||||
|
); |
||||||
|
|
||||||
|
var id = metadata?.Workflow?.Id; |
||||||
|
if (!string.IsNullOrWhiteSpace(id)) |
||||||
|
{ |
||||||
|
workflowsCache.Remove(id); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
notificationService.Show( |
||||||
|
Resources.Label_WorkflowDeleted, |
||||||
|
string.Format(Resources.Label_WorkflowDeletedSuccessfully, metadata?.Workflow?.Name) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
private void OnWorkflowInstalled(object? sender, EventArgs e) |
||||||
|
{ |
||||||
|
LoadInstalledWorkflowsAsync().SafeFireAndForget(); |
||||||
|
} |
||||||
|
|
||||||
|
public void Dispose() |
||||||
|
{ |
||||||
|
workflowsCache.Dispose(); |
||||||
|
EventManager.Instance.WorkflowInstalled -= OnWorkflowInstalled; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,347 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.ComponentModel; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using AsyncAwaitBestPractices; |
||||||
|
using Avalonia.Controls.Notifications; |
||||||
|
using CommunityToolkit.Mvvm.ComponentModel; |
||||||
|
using CommunityToolkit.Mvvm.Input; |
||||||
|
using DynamicData; |
||||||
|
using DynamicData.Binding; |
||||||
|
using FluentAvalonia.UI.Controls; |
||||||
|
using Refit; |
||||||
|
using StabilityMatrix.Avalonia.Controls; |
||||||
|
using StabilityMatrix.Avalonia.Models; |
||||||
|
using StabilityMatrix.Avalonia.Services; |
||||||
|
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||||
|
using StabilityMatrix.Avalonia.ViewModels.Dialogs; |
||||||
|
using StabilityMatrix.Avalonia.Views; |
||||||
|
using StabilityMatrix.Core.Api; |
||||||
|
using StabilityMatrix.Core.Attributes; |
||||||
|
using StabilityMatrix.Core.Helper; |
||||||
|
using StabilityMatrix.Core.Helper.Factory; |
||||||
|
using StabilityMatrix.Core.Models.Api.OpenArt; |
||||||
|
using StabilityMatrix.Core.Models.PackageModification; |
||||||
|
using StabilityMatrix.Core.Processes; |
||||||
|
using StabilityMatrix.Core.Services; |
||||||
|
using Resources = StabilityMatrix.Avalonia.Languages.Resources; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.ViewModels; |
||||||
|
|
||||||
|
[View(typeof(OpenArtBrowserPage))] |
||||||
|
[Singleton] |
||||||
|
public partial class OpenArtBrowserViewModel( |
||||||
|
IOpenArtApi openArtApi, |
||||||
|
INotificationService notificationService, |
||||||
|
ISettingsManager settingsManager, |
||||||
|
IPackageFactory packageFactory |
||||||
|
) : TabViewModelBase, IInfinitelyScroll |
||||||
|
{ |
||||||
|
private const int PageSize = 20; |
||||||
|
|
||||||
|
public override string Header => Resources.Label_OpenArtBrowser; |
||||||
|
|
||||||
|
private readonly SourceCache<OpenArtSearchResult, string> searchResultsCache = new(x => x.Id); |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[NotifyPropertyChangedFor(nameof(PageCount), nameof(CanGoBack), nameof(CanGoForward), nameof(CanGoToEnd))] |
||||||
|
private OpenArtSearchResponse? latestSearchResponse; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private IObservableCollection<OpenArtSearchResult> searchResults = |
||||||
|
new ObservableCollectionExtended<OpenArtSearchResult>(); |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private string searchQuery = string.Empty; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private bool isLoading; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[NotifyPropertyChangedFor(nameof(InternalPageNumber), nameof(CanGoBack))] |
||||||
|
private int displayedPageNumber = 1; |
||||||
|
|
||||||
|
public int InternalPageNumber => DisplayedPageNumber - 1; |
||||||
|
|
||||||
|
public int PageCount => |
||||||
|
Math.Max( |
||||||
|
1, |
||||||
|
Convert.ToInt32(Math.Ceiling((LatestSearchResponse?.Total ?? 0) / Convert.ToDouble(PageSize))) |
||||||
|
); |
||||||
|
|
||||||
|
public bool CanGoBack => |
||||||
|
string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && InternalPageNumber > 0; |
||||||
|
|
||||||
|
public bool CanGoForward => |
||||||
|
!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) || PageCount > InternalPageNumber + 1; |
||||||
|
|
||||||
|
public bool CanGoToEnd => |
||||||
|
string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && PageCount > InternalPageNumber + 1; |
||||||
|
|
||||||
|
public IEnumerable<string> AllSortModes => ["Trending", "Latest", "Most Downloaded", "Most Liked"]; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private string? selectedSortMode; |
||||||
|
|
||||||
|
protected override void OnInitialLoaded() |
||||||
|
{ |
||||||
|
searchResultsCache.Connect().DeferUntilLoaded().Bind(SearchResults).Subscribe(); |
||||||
|
SelectedSortMode = AllSortModes.First(); |
||||||
|
DoSearch().SafeFireAndForget(); |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private async Task FirstPage() |
||||||
|
{ |
||||||
|
DisplayedPageNumber = 1; |
||||||
|
searchResultsCache.Clear(); |
||||||
|
|
||||||
|
await DoSearch(); |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private async Task PreviousPage() |
||||||
|
{ |
||||||
|
DisplayedPageNumber--; |
||||||
|
searchResultsCache.Clear(); |
||||||
|
|
||||||
|
await DoSearch(InternalPageNumber); |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private async Task NextPage() |
||||||
|
{ |
||||||
|
if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) |
||||||
|
{ |
||||||
|
DisplayedPageNumber++; |
||||||
|
} |
||||||
|
|
||||||
|
searchResultsCache.Clear(); |
||||||
|
await DoSearch(InternalPageNumber); |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private async Task LastPage() |
||||||
|
{ |
||||||
|
if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) |
||||||
|
{ |
||||||
|
DisplayedPageNumber = PageCount; |
||||||
|
} |
||||||
|
|
||||||
|
searchResultsCache.Clear(); |
||||||
|
await DoSearch(PageCount - 1); |
||||||
|
} |
||||||
|
|
||||||
|
[Localizable(false)] |
||||||
|
[RelayCommand] |
||||||
|
private void OpenModel(OpenArtSearchResult workflow) |
||||||
|
{ |
||||||
|
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}"); |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private async Task SearchButton() |
||||||
|
{ |
||||||
|
DisplayedPageNumber = 1; |
||||||
|
LatestSearchResponse = null; |
||||||
|
searchResultsCache.Clear(); |
||||||
|
|
||||||
|
await DoSearch(); |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private async Task OpenWorkflow(OpenArtSearchResult workflow) |
||||||
|
{ |
||||||
|
var vm = new OpenArtWorkflowViewModel(settingsManager, packageFactory) { Workflow = workflow }; |
||||||
|
|
||||||
|
var dialog = new BetterContentDialog |
||||||
|
{ |
||||||
|
IsPrimaryButtonEnabled = true, |
||||||
|
IsSecondaryButtonEnabled = true, |
||||||
|
PrimaryButtonText = Resources.Action_Import, |
||||||
|
SecondaryButtonText = Resources.Action_Cancel, |
||||||
|
DefaultButton = ContentDialogButton.Primary, |
||||||
|
IsFooterVisible = true, |
||||||
|
MaxDialogWidth = 750, |
||||||
|
MaxDialogHeight = 850, |
||||||
|
CloseOnClickOutside = true, |
||||||
|
Content = vm |
||||||
|
}; |
||||||
|
|
||||||
|
var result = await dialog.ShowAsync(); |
||||||
|
|
||||||
|
if (result != ContentDialogResult.Primary) |
||||||
|
return; |
||||||
|
|
||||||
|
List<IPackageStep> steps = |
||||||
|
[ |
||||||
|
new DownloadOpenArtWorkflowStep(openArtApi, vm.Workflow, settingsManager) |
||||||
|
]; |
||||||
|
|
||||||
|
// Add install steps if missing nodes and preferred |
||||||
|
if ( |
||||||
|
vm is |
||||||
|
{ |
||||||
|
InstallRequiredNodes: true, |
||||||
|
MissingNodes: { Count: > 0 } missingNodes, |
||||||
|
SelectedPackage: not null, |
||||||
|
SelectedPackagePair: not null |
||||||
|
} |
||||||
|
) |
||||||
|
{ |
||||||
|
var extensionManager = vm.SelectedPackagePair.BasePackage.ExtensionManager!; |
||||||
|
|
||||||
|
steps.AddRange( |
||||||
|
missingNodes.Select( |
||||||
|
extension => |
||||||
|
new InstallExtensionStep( |
||||||
|
extensionManager, |
||||||
|
vm.SelectedPackagePair.InstalledPackage, |
||||||
|
extension |
||||||
|
) |
||||||
|
) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
var runner = new PackageModificationRunner |
||||||
|
{ |
||||||
|
ShowDialogOnStart = true, |
||||||
|
ModificationCompleteTitle = Resources.Label_WorkflowImported, |
||||||
|
ModificationCompleteMessage = Resources.Label_FinishedImportingWorkflow |
||||||
|
}; |
||||||
|
EventManager.Instance.OnPackageInstallProgressAdded(runner); |
||||||
|
|
||||||
|
await runner.ExecuteSteps(steps); |
||||||
|
|
||||||
|
notificationService.Show( |
||||||
|
Resources.Label_WorkflowImported, |
||||||
|
Resources.Label_WorkflowImportComplete, |
||||||
|
NotificationType.Success |
||||||
|
); |
||||||
|
|
||||||
|
EventManager.Instance.OnWorkflowInstalled(); |
||||||
|
} |
||||||
|
|
||||||
|
[RelayCommand] |
||||||
|
private void OpenOnOpenArt(OpenArtSearchResult? workflow) |
||||||
|
{ |
||||||
|
if (workflow?.Id == null) |
||||||
|
return; |
||||||
|
|
||||||
|
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}"); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task DoSearch(int page = 0) |
||||||
|
{ |
||||||
|
IsLoading = true; |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
OpenArtSearchResponse? response = null; |
||||||
|
if (string.IsNullOrWhiteSpace(SearchQuery)) |
||||||
|
{ |
||||||
|
var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) }; |
||||||
|
if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) |
||||||
|
{ |
||||||
|
request.Cursor = LatestSearchResponse.NextCursor; |
||||||
|
} |
||||||
|
|
||||||
|
response = await openArtApi.GetFeedAsync(request); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
response = await openArtApi.SearchAsync( |
||||||
|
new OpenArtSearchRequest |
||||||
|
{ |
||||||
|
Keyword = SearchQuery, |
||||||
|
PageSize = PageSize, |
||||||
|
CurrentPage = page |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
foreach (var item in response.Items) |
||||||
|
{ |
||||||
|
searchResultsCache.AddOrUpdate(item); |
||||||
|
} |
||||||
|
|
||||||
|
LatestSearchResponse = response; |
||||||
|
} |
||||||
|
catch (ApiException e) |
||||||
|
{ |
||||||
|
notificationService.Show(Resources.Label_ErrorRetrievingWorkflows, e.Message); |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
IsLoading = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
partial void OnSelectedSortModeChanged(string? value) |
||||||
|
{ |
||||||
|
if (value is null || SearchResults.Count == 0) |
||||||
|
return; |
||||||
|
|
||||||
|
searchResultsCache.Clear(); |
||||||
|
LatestSearchResponse = null; |
||||||
|
|
||||||
|
DoSearch().SafeFireAndForget(); |
||||||
|
} |
||||||
|
|
||||||
|
public async Task LoadNextPageAsync() |
||||||
|
{ |
||||||
|
if (!CanGoForward) |
||||||
|
return; |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
OpenArtSearchResponse? response = null; |
||||||
|
if (string.IsNullOrWhiteSpace(SearchQuery)) |
||||||
|
{ |
||||||
|
var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) }; |
||||||
|
if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) |
||||||
|
{ |
||||||
|
request.Cursor = LatestSearchResponse.NextCursor; |
||||||
|
} |
||||||
|
|
||||||
|
response = await openArtApi.GetFeedAsync(request); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
DisplayedPageNumber++; |
||||||
|
response = await openArtApi.SearchAsync( |
||||||
|
new OpenArtSearchRequest |
||||||
|
{ |
||||||
|
Keyword = SearchQuery, |
||||||
|
PageSize = PageSize, |
||||||
|
CurrentPage = InternalPageNumber |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
foreach (var item in response.Items) |
||||||
|
{ |
||||||
|
searchResultsCache.AddOrUpdate(item); |
||||||
|
} |
||||||
|
|
||||||
|
LatestSearchResponse = response; |
||||||
|
} |
||||||
|
catch (ApiException e) |
||||||
|
{ |
||||||
|
notificationService.Show("Unable to load the next page", e.Message); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static string GetSortMode(string? sortMode) |
||||||
|
{ |
||||||
|
return sortMode switch |
||||||
|
{ |
||||||
|
"Trending" => "trending", |
||||||
|
"Latest" => "latest", |
||||||
|
"Most Downloaded" => "most_downloaded", |
||||||
|
"Most Liked" => "most_liked", |
||||||
|
_ => "trending" |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue