JT
12 months ago
127 changed files with 6337 additions and 3763 deletions
@ -0,0 +1,544 @@
|
||||
[ |
||||
{ |
||||
"ModelCategory": "BaseModel", |
||||
"ModelName": "Stable Diffusion 1.5", |
||||
"RepositoryPath": "runwayml/stable-diffusion-v1-5", |
||||
"Files": [ |
||||
"v1-5-pruned-emaonly.safetensors" |
||||
], |
||||
"LicenseType": "CreativeML Open RAIL-M" |
||||
}, |
||||
{ |
||||
"ModelCategory": "BaseModel", |
||||
"ModelName": "Stable Diffusion 2.1", |
||||
"RepositoryPath": "stabilityai/stable-diffusion-2-1", |
||||
"Files": [ |
||||
"v2-1_768-ema-pruned.safetensors" |
||||
], |
||||
"LicenseType": "Open RAIL++" |
||||
}, |
||||
{ |
||||
"ModelCategory": "BaseModel", |
||||
"ModelName": "Stable Diffusion XL (Base)", |
||||
"RepositoryPath": "stabilityai/stable-diffusion-xl-base-1.0", |
||||
"Files": [ |
||||
"sd_xl_base_1.0_0.9vae.safetensors", |
||||
"sd_xl_offset_example-lora_1.0.safetensors" |
||||
], |
||||
"LicenseType": "Open RAIL++", |
||||
"LicensePath": "LICENSE.md" |
||||
}, |
||||
{ |
||||
"ModelCategory": "BaseModel", |
||||
"ModelName": "Stable Diffusion XL (Refiner)", |
||||
"RepositoryPath": "stabilityai/stable-diffusion-xl-refiner-1.0", |
||||
"Files": [ |
||||
"sd_xl_refiner_1.0_0.9vae.safetensors" |
||||
], |
||||
"LicenseType": "Open RAIL++", |
||||
"LicensePath": "LICENSE.md" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "Canny", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11p_sd15_canny.pth", |
||||
"control_v11p_sd15_canny.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "Depth", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11f1p_sd15_depth.pth", |
||||
"control_v11f1p_sd15_depth.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "MLSD", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11p_sd15_mlsd.pth", |
||||
"control_v11p_sd15_mlsd.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "Inpaint", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11p_sd15_inpaint.pth", |
||||
"control_v11p_sd15_inpaint.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "IP2P", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11e_sd15_ip2p.pth", |
||||
"control_v11e_sd15_ip2p.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "Tile", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11f1e_sd15_tile.pth", |
||||
"control_v11f1e_sd15_tile.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "OpenPose", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11p_sd15_openpose.pth", |
||||
"control_v11p_sd15_openpose.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "LineArt", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11p_sd15_lineart.pth", |
||||
"control_v11p_sd15_lineart.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "LineArt Anime", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11p_sd15s2_lineart_anime.pth", |
||||
"control_v11p_sd15s2_lineart_anime.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "NormalBae", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11p_sd15_normalbae.pth", |
||||
"control_v11p_sd15_normalbae.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "Seg", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11p_sd15_seg.pth", |
||||
"control_v11p_sd15_seg.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "Soft Edge", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11p_sd15_softedge.pth", |
||||
"control_v11p_sd15_softedge.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "Scribble", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11p_sd15_scribble.pth", |
||||
"control_v11p_sd15_scribble.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "ControlNet", |
||||
"ModelName": "Shuffle", |
||||
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||
"Files": [ |
||||
"control_v11e_sd15_shuffle.pth", |
||||
"control_v11e_sd15_shuffle.yaml" |
||||
], |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "Canny", |
||||
"RepositoryPath": "lllyasviel/control_v11p_sd15_canny", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "canny", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "Depth", |
||||
"RepositoryPath": "lllyasviel/control_v11f1p_sd15_depth", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "depth", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "MLSD", |
||||
"RepositoryPath": "lllyasviel/control_v11p_sd15_mlsd", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "MLSD", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "Inpaint", |
||||
"RepositoryPath": "lllyasviel/control_v11p_sd15_inpaint", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "inpaint", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "IP2P", |
||||
"RepositoryPath": "lllyasviel/control_v11e_sd15_ip2p", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "ip2p", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "Tile", |
||||
"RepositoryPath": "lllyasviel/control_v11f1e_sd15_tile", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.bin", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "tile", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "OpenPose", |
||||
"RepositoryPath": "lllyasviel/control_v11p_sd15_openpose", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "openpose", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "LineArt", |
||||
"RepositoryPath": "lllyasviel/control_v11p_sd15_lineart", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "lineart", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "LineArt Anime", |
||||
"RepositoryPath": "lllyasviel/control_v11p_sd15s2_lineart_anime", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "lineart_anime", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "NormalBae", |
||||
"RepositoryPath": "lllyasviel/control_v11p_sd15_normalbae", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "normalbae", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "Seg", |
||||
"RepositoryPath": "lllyasviel/control_v11p_sd15_seg", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "seg", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "Soft Edge", |
||||
"RepositoryPath": "lllyasviel/control_v11p_sd15_softedge", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "softedge", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "Scribble", |
||||
"RepositoryPath": "lllyasviel/control_v11p_sd15_scribble", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "scribble", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersControlNet", |
||||
"ModelName": "Shuffle", |
||||
"RepositoryPath": "lllyasviel/control_v11e_sd15_shuffle", |
||||
"Files": [ |
||||
"diffusion_pytorch_model.safetensors", |
||||
"config.json" |
||||
], |
||||
"Subfolder": "shuffle", |
||||
"LicenseType": "Open RAIL" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersClipVision", |
||||
"ModelName": "IP Adapter Encoder", |
||||
"RepositoryPath": "InvokeAI/ip_adapter_sd_image_encoder", |
||||
"Files": [ |
||||
"config.json", |
||||
"model.safetensors" |
||||
], |
||||
"Subfolder": "ip_adapter_sd_image_encoder", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersClipVision", |
||||
"ModelName": "IP Adapter Encoder (SDXL)", |
||||
"RepositoryPath": "InvokeAI/ip_adapter_sdxl_image_encoder", |
||||
"Files": [ |
||||
"config.json", |
||||
"model.safetensors" |
||||
], |
||||
"Subfolder": "ip_adapter_sd_image_encoder", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersIpAdapter", |
||||
"ModelName": "SD 1.5 Adapter", |
||||
"RepositoryPath": "InvokeAI/ip_adapter_sd15", |
||||
"Files": [ |
||||
"image_encoder.txt", |
||||
"ip_adapter.bin" |
||||
], |
||||
"Subfolder": "ip_adapter_sd15", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersIpAdapter", |
||||
"ModelName": "SD 1.5 Light Adapter", |
||||
"RepositoryPath": "InvokeAI/ip_adapter_sd15_light", |
||||
"Files": [ |
||||
"image_encoder.txt", |
||||
"ip_adapter.bin" |
||||
], |
||||
"Subfolder": "ip_adapter_sd15_light", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersIpAdapter", |
||||
"ModelName": "SD 1.5 Plus Adapter", |
||||
"RepositoryPath": "InvokeAI/ip_adapter_plus_sd15", |
||||
"Files": [ |
||||
"image_encoder.txt", |
||||
"ip_adapter.bin" |
||||
], |
||||
"Subfolder": "ip_adapter_plus_sd15", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersIpAdapter", |
||||
"ModelName": "SD 1.5 Face Plus Adapter", |
||||
"RepositoryPath": "InvokeAI/ip_adapter_plus_face_sd15", |
||||
"Files": [ |
||||
"image_encoder.txt", |
||||
"ip_adapter.bin" |
||||
], |
||||
"Subfolder": "ip_adapter_plus_face_sd15", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersIpAdapterXl", |
||||
"ModelName": "SDXL Adapter", |
||||
"RepositoryPath": "InvokeAI/ip_adapter_sdxl", |
||||
"Files": [ |
||||
"image_encoder.txt", |
||||
"ip_adapter.bin" |
||||
], |
||||
"Subfolder": "ip_adapter_sdxl", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersIpAdapterXl", |
||||
"ModelName": "SDXL Plus Adapter", |
||||
"RepositoryPath": "InvokeAI/ip-adapter-plus_sdxl_vit-h", |
||||
"Files": [ |
||||
"image_encoder.txt", |
||||
"ip_adapter.bin" |
||||
], |
||||
"Subfolder": "ip_adapter_plus_sdxl_vit-h", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersIpAdapterXl", |
||||
"ModelName": "SDXL Face Plus Adapter", |
||||
"RepositoryPath": "InvokeAI/ip-adapter-plus-face_sdxl_vit-h", |
||||
"Files": [ |
||||
"image_encoder.txt", |
||||
"ip_adapter.bin" |
||||
], |
||||
"Subfolder": "ip_adapter_plus_face_sdxl_vit-h", |
||||
"LicenseType": "CreativeML Open RAIL-M" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersT2IAdapter", |
||||
"ModelName": "Sketch", |
||||
"RepositoryPath": "TencentARC/t2iadapter_sketch_sd15v2", |
||||
"Files": [ |
||||
"config.json", |
||||
"diffusion_pytorch_model.bin" |
||||
], |
||||
"Subfolder": "t2iadapter_sketch_sd15v2", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersT2IAdapter", |
||||
"ModelName": "Depth", |
||||
"RepositoryPath": "TencentARC/t2iadapter_depth_sd15v2", |
||||
"Files": [ |
||||
"config.json", |
||||
"diffusion_pytorch_model.bin" |
||||
], |
||||
"Subfolder": "t2iadapter_depth_sd15v2", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersT2IAdapter", |
||||
"ModelName": "Canny", |
||||
"RepositoryPath": "TencentARC/t2iadapter_canny_sd15v2", |
||||
"Files": [ |
||||
"config.json", |
||||
"diffusion_pytorch_model.bin" |
||||
], |
||||
"Subfolder": "t2iadapter_canny_sd15v2", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersT2IAdapter", |
||||
"ModelName": "Depth-Zoe", |
||||
"RepositoryPath": "TencentARC/t2iadapter_zoedepth_sd15v1", |
||||
"Files": [ |
||||
"config.json", |
||||
"diffusion_pytorch_model.bin" |
||||
], |
||||
"Subfolder": "t2iadapter_zoedepth_sd15v1", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersT2IAdapter", |
||||
"ModelName": "Sketch (SDXL)", |
||||
"RepositoryPath": "TencentARC/t2i-adapter-sketch-sdxl-1.0", |
||||
"Files": [ |
||||
"config.json", |
||||
"diffusion_pytorch_model.safetensors" |
||||
], |
||||
"Subfolder": "t2i-adapter-sketch-sdxl-1.0", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersT2IAdapter", |
||||
"ModelName": "Depth-Zoe (SDXL)", |
||||
"RepositoryPath": "TencentARC/t2i-adapter-depth-zoe-sdxl-1.0", |
||||
"Files": [ |
||||
"config.json", |
||||
"diffusion_pytorch_model.safetensors" |
||||
], |
||||
"Subfolder": "t2i-adapter-depth-zoe-sdxl-1.0", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersT2IAdapter", |
||||
"ModelName": "OpenPose (SDXL)", |
||||
"RepositoryPath": "TencentARC/t2i-adapter-openpose-sdxl-1.0", |
||||
"Files": [ |
||||
"config.json", |
||||
"diffusion_pytorch_model.safetensors" |
||||
], |
||||
"Subfolder": "t2i-adapter-openpose-sdxl-1.0", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersT2IAdapter", |
||||
"ModelName": "Depth-MiDaS (SDXL)", |
||||
"RepositoryPath": "TencentARC/t2i-adapter-depth-midas-sdxl-1.0", |
||||
"Files": [ |
||||
"config.json", |
||||
"diffusion_pytorch_model.safetensors" |
||||
], |
||||
"Subfolder": "t2i-adapter-depth-midas-sdxl-1.0", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersT2IAdapter", |
||||
"ModelName": "LineArt (SDXL)", |
||||
"RepositoryPath": "TencentARC/t2i-adapter-lineart-sdxl-1.0", |
||||
"Files": [ |
||||
"config.json", |
||||
"diffusion_pytorch_model.safetensors" |
||||
], |
||||
"Subfolder": "t2i-adapter-lineart-sdxl-1.0", |
||||
"LicenseType": "Apache 2.0" |
||||
}, |
||||
{ |
||||
"ModelCategory": "DiffusersT2IAdapter", |
||||
"ModelName": "Canny (SDXL)", |
||||
"RepositoryPath": "TencentARC/t2i-adapter-canny-sdxl-1.0", |
||||
"Files": [ |
||||
"config.json", |
||||
"diffusion_pytorch_model.safetensors" |
||||
], |
||||
"Subfolder": "t2i-adapter-canny-sdxl-1.0", |
||||
"LicenseType": "Apache 2.0" |
||||
} |
||||
] |
@ -0,0 +1,113 @@
|
||||
using System.Linq; |
||||
using Avalonia; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Input; |
||||
using Avalonia.Interactivity; |
||||
using Avalonia.VisualTree; |
||||
using JetBrains.Annotations; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls.Extensions; |
||||
|
||||
/// <summary> |
||||
/// Show tooltip on Controls with IsEffectivelyEnabled = false |
||||
/// https://github.com/AvaloniaUI/Avalonia/issues/3847#issuecomment-1618790059 |
||||
/// </summary> |
||||
[PublicAPI] |
||||
public static class ShowDisabledTooltipExtension |
||||
{ |
||||
static ShowDisabledTooltipExtension() |
||||
{ |
||||
ShowOnDisabledProperty.Changed.AddClassHandler<Control>(HandleShowOnDisabledChanged); |
||||
} |
||||
|
||||
public static bool GetShowOnDisabled(AvaloniaObject obj) |
||||
{ |
||||
return obj.GetValue(ShowOnDisabledProperty); |
||||
} |
||||
|
||||
public static void SetShowOnDisabled(AvaloniaObject obj, bool value) |
||||
{ |
||||
obj.SetValue(ShowOnDisabledProperty, value); |
||||
} |
||||
|
||||
public static readonly AttachedProperty<bool> ShowOnDisabledProperty = AvaloniaProperty.RegisterAttached< |
||||
object, |
||||
Control, |
||||
bool |
||||
>("ShowOnDisabled"); |
||||
|
||||
private static void HandleShowOnDisabledChanged(Control control, AvaloniaPropertyChangedEventArgs e) |
||||
{ |
||||
if (e.GetNewValue<bool>()) |
||||
{ |
||||
control.DetachedFromVisualTree += AttachedControl_DetachedFromVisualOrExtension; |
||||
control.AttachedToVisualTree += AttachedControl_AttachedToVisualTree; |
||||
if (control.IsInitialized) |
||||
{ |
||||
// enabled after visual attached |
||||
AttachedControl_AttachedToVisualTree(control, null!); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
AttachedControl_DetachedFromVisualOrExtension(control, null!); |
||||
} |
||||
} |
||||
|
||||
private static void AttachedControl_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) |
||||
{ |
||||
if (sender is not Control control || TopLevel.GetTopLevel(control) is not { } tl) |
||||
{ |
||||
return; |
||||
} |
||||
// NOTE pointermove needed to be tunneled for me but you may not need to... |
||||
tl.AddHandler(InputElement.PointerMovedEvent, TopLevel_PointerMoved, RoutingStrategies.Tunnel); |
||||
} |
||||
|
||||
private static void AttachedControl_DetachedFromVisualOrExtension(object? s, VisualTreeAttachmentEventArgs e) |
||||
{ |
||||
if (s is not Control control) |
||||
{ |
||||
return; |
||||
} |
||||
control.DetachedFromVisualTree -= AttachedControl_DetachedFromVisualOrExtension; |
||||
control.AttachedToVisualTree -= AttachedControl_AttachedToVisualTree; |
||||
if (TopLevel.GetTopLevel(control) is not { } tl) |
||||
{ |
||||
return; |
||||
} |
||||
tl.RemoveHandler(InputElement.PointerMovedEvent, TopLevel_PointerMoved); |
||||
} |
||||
|
||||
private static void TopLevel_PointerMoved(object? sender, PointerEventArgs e) |
||||
{ |
||||
if (sender is not Control tl) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
var attachedControls = tl.GetVisualDescendants().Where(GetShowOnDisabled).Cast<Control>().ToList(); |
||||
|
||||
// find disabled children under pointer w/ this extension enabled |
||||
var disabledChildUnderPointer = attachedControls.FirstOrDefault( |
||||
x => |
||||
x.Bounds.Contains(e.GetPosition(x.Parent as Visual)) |
||||
&& x is { IsEffectivelyVisible: true, IsEffectivelyEnabled: false } |
||||
); |
||||
|
||||
if (disabledChildUnderPointer != null) |
||||
{ |
||||
// manually show tooltip |
||||
ToolTip.SetIsOpen(disabledChildUnderPointer, true); |
||||
} |
||||
|
||||
var disabledTooltipsToHide = attachedControls.Where( |
||||
x => ToolTip.GetIsOpen(x) && x != disabledChildUnderPointer && !x.IsEffectivelyEnabled |
||||
); |
||||
|
||||
foreach (var control in disabledTooltipsToHide) |
||||
{ |
||||
ToolTip.SetIsOpen(control, false); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,124 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using Avalonia; |
||||
using Avalonia.PropertyGrid.Services; |
||||
using JetBrains.Annotations; |
||||
using PropertyModels.ComponentModel; |
||||
using StabilityMatrix.Core.Extensions; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls; |
||||
|
||||
/// <inheritdoc /> |
||||
[PublicAPI] |
||||
public class BetterPropertyGrid : global::Avalonia.PropertyGrid.Controls.PropertyGrid |
||||
{ |
||||
protected override Type StyleKeyOverride => typeof(global::Avalonia.PropertyGrid.Controls.PropertyGrid); |
||||
|
||||
public static readonly StyledProperty<IEnumerable<string>> ExcludedCategoriesProperty = AvaloniaProperty.Register< |
||||
BetterPropertyGrid, |
||||
IEnumerable<string> |
||||
>("ExcludedCategories"); |
||||
|
||||
public IEnumerable<string> ExcludedCategories |
||||
{ |
||||
get => GetValue(ExcludedCategoriesProperty); |
||||
set => SetValue(ExcludedCategoriesProperty, value); |
||||
} |
||||
|
||||
public static readonly StyledProperty<IEnumerable<string>> IncludedCategoriesProperty = AvaloniaProperty.Register< |
||||
BetterPropertyGrid, |
||||
IEnumerable<string> |
||||
>("IncludedCategories"); |
||||
|
||||
public IEnumerable<string> IncludedCategories |
||||
{ |
||||
get => GetValue(IncludedCategoriesProperty); |
||||
set => SetValue(IncludedCategoriesProperty, value); |
||||
} |
||||
|
||||
static BetterPropertyGrid() |
||||
{ |
||||
// Register factories |
||||
CellEditFactoryService.Default.AddFactory(new ToggleSwitchCellEditFactory()); |
||||
|
||||
// Initialize localization and name resolver |
||||
LocalizationService.Default.AddExtraService(new PropertyGridLocalizationService()); |
||||
|
||||
ExcludedCategoriesProperty |
||||
.Changed |
||||
.AddClassHandler<BetterPropertyGrid>( |
||||
(grid, args) => |
||||
{ |
||||
if (args.NewValue is IEnumerable<string> excludedCategories) |
||||
{ |
||||
grid.FilterExcludeCategories(excludedCategories); |
||||
} |
||||
} |
||||
); |
||||
|
||||
IncludedCategoriesProperty |
||||
.Changed |
||||
.AddClassHandler<BetterPropertyGrid>( |
||||
(grid, args) => |
||||
{ |
||||
if (args.NewValue is IEnumerable<string> includedCategories) |
||||
{ |
||||
grid.FilterIncludeCategories(includedCategories); |
||||
} |
||||
} |
||||
); |
||||
} |
||||
|
||||
public void FilterExcludeCategories(IEnumerable<string> excludedCategories) |
||||
{ |
||||
// Get internal property `ViewModel` of internal type `PropertyGridViewModel` |
||||
var gridVm = this.GetProtectedProperty("ViewModel")!; |
||||
// Get public property `CategoryFilter` |
||||
var categoryFilter = gridVm.GetProtectedProperty<CheckedMaskModel>("CategoryFilter")!; |
||||
|
||||
categoryFilter.BeginUpdate(); |
||||
|
||||
// Uncheck All, then check all except All |
||||
categoryFilter.UnCheck(categoryFilter.All); |
||||
|
||||
foreach (var mask in categoryFilter.Masks.Where(m => m != categoryFilter.All)) |
||||
{ |
||||
categoryFilter.Check(mask); |
||||
} |
||||
|
||||
// Uncheck excluded categories |
||||
foreach (var mask in excludedCategories) |
||||
{ |
||||
categoryFilter.UnCheck(mask); |
||||
} |
||||
|
||||
categoryFilter.EndUpdate(); |
||||
} |
||||
|
||||
public void FilterIncludeCategories(IEnumerable<string> includeCategories) |
||||
{ |
||||
// Get internal property `ViewModel` of internal type `PropertyGridViewModel` |
||||
var gridVm = this.GetProtectedProperty("ViewModel")!; |
||||
// Get public property `CategoryFilter` |
||||
var categoryFilter = gridVm.GetProtectedProperty<CheckedMaskModel>("CategoryFilter")!; |
||||
|
||||
categoryFilter.BeginUpdate(); |
||||
|
||||
// Uncheck non-included categories |
||||
foreach (var mask in categoryFilter.Masks.Where(m => !includeCategories.Contains(m))) |
||||
{ |
||||
categoryFilter.UnCheck(mask); |
||||
} |
||||
|
||||
categoryFilter.UnCheck(categoryFilter.All); |
||||
|
||||
// Check included categories |
||||
foreach (var mask in includeCategories) |
||||
{ |
||||
categoryFilter.Check(mask); |
||||
} |
||||
|
||||
categoryFilter.EndUpdate(); |
||||
} |
||||
} |
@ -0,0 +1,35 @@
|
||||
using System; |
||||
using System.Globalization; |
||||
using PropertyModels.Localilzation; |
||||
using StabilityMatrix.Avalonia.Languages; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls; |
||||
|
||||
internal class PropertyGridCultureData : ICultureData |
||||
{ |
||||
/// <inheritdoc /> |
||||
public bool Reload() => false; |
||||
|
||||
/// <inheritdoc /> |
||||
public CultureInfo Culture => Cultures.Current ?? Cultures.Default; |
||||
|
||||
/// <inheritdoc /> |
||||
public Uri Path => new(""); |
||||
|
||||
/// <inheritdoc /> |
||||
public string this[string key] |
||||
{ |
||||
get |
||||
{ |
||||
if (Resources.ResourceManager.GetString(key) is { } result) |
||||
{ |
||||
return result; |
||||
} |
||||
|
||||
return key; |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public bool IsLoaded => true; |
||||
} |
@ -0,0 +1,36 @@
|
||||
using System; |
||||
using PropertyModels.ComponentModel; |
||||
using PropertyModels.Localilzation; |
||||
using StabilityMatrix.Avalonia.Languages; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls; |
||||
|
||||
/// <summary> |
||||
/// Implements <see cref="ILocalizationService"/> using static <see cref="Cultures"/>. |
||||
/// </summary> |
||||
internal class PropertyGridLocalizationService : MiniReactiveObject, ILocalizationService |
||||
{ |
||||
/// <inheritdoc /> |
||||
public ICultureData CultureData { get; } = new PropertyGridCultureData(); |
||||
|
||||
/// <inheritdoc /> |
||||
public string this[string key] => CultureData[key]; |
||||
|
||||
/// <inheritdoc /> |
||||
public event EventHandler? OnCultureChanged; |
||||
|
||||
/// <inheritdoc /> |
||||
public ILocalizationService[] GetExtraServices() => Array.Empty<ILocalizationService>(); |
||||
|
||||
/// <inheritdoc /> |
||||
public void AddExtraService(ILocalizationService service) { } |
||||
|
||||
/// <inheritdoc /> |
||||
public void RemoveExtraService(ILocalizationService service) { } |
||||
|
||||
/// <inheritdoc /> |
||||
public ICultureData[] GetCultures() => new[] { CultureData }; |
||||
|
||||
/// <inheritdoc /> |
||||
public void SelectCulture(string cultureName) { } |
||||
} |
@ -0,0 +1,60 @@
|
||||
using Avalonia.Controls; |
||||
using Avalonia.PropertyGrid.Controls; |
||||
using Avalonia.PropertyGrid.Controls.Factories; |
||||
using Avalonia.PropertyGrid.Localization; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls; |
||||
|
||||
internal class ToggleSwitchCellEditFactory : AbstractCellEditFactory |
||||
{ |
||||
// make this extend factor only effect on TestExtendPropertyGrid |
||||
public override bool Accept(object accessToken) |
||||
{ |
||||
return accessToken is BetterPropertyGrid; |
||||
} |
||||
|
||||
public override Control? HandleNewProperty(PropertyCellContext context) |
||||
{ |
||||
var propertyDescriptor = context.Property; |
||||
var target = context.Target; |
||||
|
||||
if (propertyDescriptor.PropertyType != typeof(bool)) |
||||
{ |
||||
return null; |
||||
} |
||||
|
||||
var control = new ToggleSwitch(); |
||||
control.SetLocalizeBinding(ToggleSwitch.OnContentProperty, "On"); |
||||
control.SetLocalizeBinding(ToggleSwitch.OffContentProperty, "Off"); |
||||
|
||||
control.IsCheckedChanged += (s, e) => |
||||
{ |
||||
SetAndRaise(context, control, control.IsChecked); |
||||
}; |
||||
|
||||
return control; |
||||
} |
||||
|
||||
public override bool HandlePropertyChanged(PropertyCellContext context) |
||||
{ |
||||
var propertyDescriptor = context.Property; |
||||
var target = context.Target; |
||||
var control = context.CellEdit; |
||||
|
||||
if (propertyDescriptor.PropertyType != typeof(bool)) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
ValidateProperty(control, propertyDescriptor, target); |
||||
|
||||
if (control is ToggleSwitch ts) |
||||
{ |
||||
ts.IsChecked = (bool)(propertyDescriptor.GetValue(target) ?? false); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,22 @@
|
||||
using System; |
||||
using System.Diagnostics.CodeAnalysis; |
||||
using System.Globalization; |
||||
using Avalonia.Data.Converters; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Converters; |
||||
|
||||
public class CustomStringFormatConverter<T>([StringSyntax("CompositeFormat")] string format) : IValueConverter |
||||
where T : IFormatProvider, new() |
||||
{ |
||||
/// <inheritdoc /> |
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||
{ |
||||
return value is null ? null : string.Format(new T(), format, value); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||
{ |
||||
return value is null ? null : throw new NotImplementedException(); |
||||
} |
||||
} |
@ -0,0 +1,31 @@
|
||||
using System; |
||||
using System.Globalization; |
||||
using Avalonia.Data.Converters; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Converters; |
||||
|
||||
/// <summary> |
||||
/// Converts an index to index + 1 |
||||
/// </summary> |
||||
public class IndexPlusOneConverter : IValueConverter |
||||
{ |
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||
{ |
||||
if (value is int i) |
||||
{ |
||||
return i + 1; |
||||
} |
||||
|
||||
return value; |
||||
} |
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||
{ |
||||
if (value is int i) |
||||
{ |
||||
return i - 1; |
||||
} |
||||
|
||||
return value; |
||||
} |
||||
} |
@ -0,0 +1,41 @@
|
||||
using System; |
||||
using Size = StabilityMatrix.Core.Helper.Size; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Converters; |
||||
|
||||
public class MemoryBytesFormatter : ICustomFormatter, IFormatProvider |
||||
{ |
||||
/// <inheritdoc /> |
||||
public object? GetFormat(Type? formatType) |
||||
{ |
||||
return formatType == typeof(ICustomFormatter) ? this : null; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public string Format(string? format, object? arg, IFormatProvider? formatProvider) |
||||
{ |
||||
if (format == null || !format.Trim().StartsWith('M')) |
||||
{ |
||||
if (arg is IFormattable formatArg) |
||||
{ |
||||
return formatArg.ToString(format, formatProvider); |
||||
} |
||||
|
||||
return arg?.ToString() ?? string.Empty; |
||||
} |
||||
|
||||
var value = Convert.ToUInt64(arg); |
||||
|
||||
var result = format.Trim().EndsWith("10", StringComparison.OrdinalIgnoreCase) |
||||
? Size.FormatBase10Bytes(value) |
||||
: Size.FormatBytes(value); |
||||
|
||||
// Strip i if not Mi |
||||
if (!format.Trim().Contains('I', StringComparison.OrdinalIgnoreCase)) |
||||
{ |
||||
result = result.Replace("i", string.Empty, StringComparison.OrdinalIgnoreCase); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
@ -0,0 +1,30 @@
|
||||
using System; |
||||
using System.Threading.Tasks; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.DesignData; |
||||
|
||||
public class MockMetadataImportService : IMetadataImportService |
||||
{ |
||||
public Task ScanDirectoryForMissingInfo(DirectoryPath directory, IProgress<ProgressReport>? progress = null) |
||||
{ |
||||
return Task.CompletedTask; |
||||
} |
||||
|
||||
public Task<ConnectedModelInfo?> GetMetadataForFile( |
||||
FilePath filePath, |
||||
IProgress<ProgressReport>? progress = null, |
||||
bool forceReimport = false |
||||
) |
||||
{ |
||||
return null; |
||||
} |
||||
|
||||
public Task UpdateExistingMetadata(DirectoryPath directory, IProgress<ProgressReport>? progress = null) |
||||
{ |
||||
return Task.CompletedTask; |
||||
} |
||||
} |
@ -0,0 +1,54 @@
|
||||
using System.ComponentModel; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using PropertyModels.ComponentModel; |
||||
using StabilityMatrix.Avalonia.Languages; |
||||
|
||||
#pragma warning disable CS0657 // Not a valid attribute location for this declaration |
||||
|
||||
namespace StabilityMatrix.Avalonia.DesignData; |
||||
|
||||
public partial class MockPropertyGridObject : ObservableObject |
||||
{ |
||||
[ObservableProperty] |
||||
private string? stringProperty; |
||||
|
||||
[ObservableProperty] |
||||
private int intProperty; |
||||
|
||||
[ObservableProperty] |
||||
[property: Trackable(0, 50, Increment = 1, FormatString = "{0:0}")] |
||||
private int intRange = 10; |
||||
|
||||
[ObservableProperty] |
||||
[property: Trackable(0d, 1d, Increment = 0.01, FormatString = "{0:P0}")] |
||||
private double floatPercentRange = 0.25; |
||||
|
||||
[ObservableProperty] |
||||
[property: DisplayName("Int Custom Name")] |
||||
private int intCustomNameProperty = 42; |
||||
|
||||
[ObservableProperty] |
||||
[property: DisplayName(nameof(Resources.Label_Language))] |
||||
private int? intLocalizedNameProperty; |
||||
|
||||
[ObservableProperty] |
||||
private bool boolProperty; |
||||
|
||||
[ObservableProperty] |
||||
[property: Category("Included Category")] |
||||
private string? stringIncludedCategoryProperty; |
||||
|
||||
[ObservableProperty] |
||||
[property: Category("Excluded Category")] |
||||
private string? stringExcludedCategoryProperty; |
||||
} |
||||
|
||||
public partial class MockPropertyGridObjectAlt : ObservableObject |
||||
{ |
||||
[ObservableProperty] |
||||
private int altIntProperty = 10; |
||||
|
||||
[ObservableProperty] |
||||
[property: Category("Settings")] |
||||
private string? altStringProperty; |
||||
} |
@ -0,0 +1,46 @@
|
||||
using System.Text.Json.Serialization; |
||||
using StabilityMatrix.Core.Converters.Json; |
||||
using StabilityMatrix.Core.Extensions; |
||||
using StabilityMatrix.Core.Models; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Models.HuggingFace; |
||||
|
||||
[JsonConverter(typeof(DefaultUnknownEnumConverter<HuggingFaceModelType>))] |
||||
public enum HuggingFaceModelType |
||||
{ |
||||
[Description("Base Models")] |
||||
[ConvertTo<SharedFolderType>(SharedFolderType.StableDiffusion)] |
||||
BaseModel, |
||||
|
||||
[Description("ControlNets")] |
||||
[ConvertTo<SharedFolderType>(SharedFolderType.ControlNet)] |
||||
ControlNet, |
||||
|
||||
[Description("ControlNets (Diffusers)")] |
||||
[ConvertTo<SharedFolderType>(SharedFolderType.ControlNet)] |
||||
DiffusersControlNet, |
||||
|
||||
[Description("IP Adapters")] |
||||
[ConvertTo<SharedFolderType>(SharedFolderType.IpAdapter)] |
||||
IpAdapter, |
||||
|
||||
[Description("IP Adapters (Diffusers SD1.5)")] |
||||
[ConvertTo<SharedFolderType>(SharedFolderType.InvokeIpAdapters15)] |
||||
DiffusersIpAdapter, |
||||
|
||||
[Description("IP Adapters (Diffusers SDXL)")] |
||||
[ConvertTo<SharedFolderType>(SharedFolderType.InvokeIpAdaptersXl)] |
||||
DiffusersIpAdapterXl, |
||||
|
||||
[Description("CLIP Vision (Diffusers)")] |
||||
[ConvertTo<SharedFolderType>(SharedFolderType.InvokeClipVision)] |
||||
DiffusersClipVision, |
||||
|
||||
[Description("T2I Adapters")] |
||||
[ConvertTo<SharedFolderType>(SharedFolderType.T2IAdapter)] |
||||
T2IAdapter, |
||||
|
||||
[Description("T2I Adapters (Diffusers)")] |
||||
[ConvertTo<SharedFolderType>(SharedFolderType.T2IAdapter)] |
||||
DiffusersT2IAdapter, |
||||
} |
@ -0,0 +1,12 @@
|
||||
namespace StabilityMatrix.Avalonia.Models.HuggingFace; |
||||
|
||||
public class HuggingfaceItem |
||||
{ |
||||
public required HuggingFaceModelType ModelCategory { get; set; } |
||||
public required string ModelName { get; set; } |
||||
public required string RepositoryPath { get; set; } |
||||
public required string[] Files { get; set; } |
||||
public required string LicenseType { get; set; } |
||||
public string? LicensePath { get; set; } |
||||
public string? Subfolder { get; set; } |
||||
} |
@ -1,27 +1,43 @@
|
||||
using System; |
||||
using Avalonia.Threading; |
||||
using FluentAvalonia.UI.Controls; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Base; |
||||
|
||||
public class ContentDialogViewModelBase : ViewModelBase |
||||
{ |
||||
public virtual string? Title { get; set; } |
||||
|
||||
// Events for button clicks |
||||
public event EventHandler<ContentDialogResult>? PrimaryButtonClick; |
||||
public event EventHandler<ContentDialogResult>? SecondaryButtonClick; |
||||
public event EventHandler<ContentDialogResult>? CloseButtonClick; |
||||
|
||||
|
||||
public virtual void OnPrimaryButtonClick() |
||||
{ |
||||
PrimaryButtonClick?.Invoke(this, ContentDialogResult.Primary); |
||||
} |
||||
|
||||
|
||||
public virtual void OnSecondaryButtonClick() |
||||
{ |
||||
SecondaryButtonClick?.Invoke(this, ContentDialogResult.Secondary); |
||||
} |
||||
|
||||
|
||||
public virtual void OnCloseButtonClick() |
||||
{ |
||||
CloseButtonClick?.Invoke(this, ContentDialogResult.None); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Return a <see cref="BetterContentDialog"/> that uses this view model as its content |
||||
/// </summary> |
||||
public virtual BetterContentDialog GetDialog() |
||||
{ |
||||
Dispatcher.UIThread.VerifyAccess(); |
||||
|
||||
var dialog = new BetterContentDialog { Title = Title, Content = this }; |
||||
|
||||
return dialog; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,6 @@
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Base; |
||||
|
||||
public abstract class TabViewModelBase : ViewModelBase |
||||
{ |
||||
public abstract string Header { get; } |
||||
} |
@ -0,0 +1,612 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Collections.ObjectModel; |
||||
using System.ComponentModel; |
||||
using System.Diagnostics; |
||||
using System.Linq; |
||||
using System.Net.Http; |
||||
using System.Reactive; |
||||
using System.Reactive.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using AsyncAwaitBestPractices; |
||||
using Avalonia.Collections; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Controls.Notifications; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using LiteDB; |
||||
using LiteDB.Async; |
||||
using NLog; |
||||
using Refit; |
||||
using StabilityMatrix.Avalonia.Languages; |
||||
using StabilityMatrix.Avalonia.Services; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; |
||||
using StabilityMatrix.Avalonia.Views; |
||||
using StabilityMatrix.Core.Api; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Database; |
||||
using StabilityMatrix.Core.Extensions; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Helper.Cache; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Api; |
||||
using StabilityMatrix.Core.Models.Settings; |
||||
using StabilityMatrix.Core.Services; |
||||
using Notification = Avalonia.Controls.Notifications.Notification; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; |
||||
|
||||
[View(typeof(CivitAiBrowserPage))] |
||||
[Singleton] |
||||
public partial class CivitAiBrowserViewModel : TabViewModelBase |
||||
{ |
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
||||
private readonly ICivitApi civitApi; |
||||
private readonly IDownloadService downloadService; |
||||
private readonly ISettingsManager settingsManager; |
||||
private readonly ServiceManager<ViewModelBase> dialogFactory; |
||||
private readonly ILiteDbContext liteDbContext; |
||||
private readonly INotificationService notificationService; |
||||
private const int MaxModelsPerPage = 20; |
||||
private LRUCache< |
||||
int /* model id */ |
||||
, |
||||
CheckpointBrowserCardViewModel |
||||
> cache = new(50); |
||||
|
||||
[ObservableProperty] |
||||
private ObservableCollection<CheckpointBrowserCardViewModel>? modelCards; |
||||
|
||||
[ObservableProperty] |
||||
private DataGridCollectionView? modelCardsView; |
||||
|
||||
[ObservableProperty] |
||||
private string searchQuery = string.Empty; |
||||
|
||||
[ObservableProperty] |
||||
private bool showNsfw; |
||||
|
||||
[ObservableProperty] |
||||
private bool showMainLoadingSpinner; |
||||
|
||||
[ObservableProperty] |
||||
private CivitPeriod selectedPeriod = CivitPeriod.Month; |
||||
|
||||
[ObservableProperty] |
||||
private CivitSortMode sortMode = CivitSortMode.HighestRated; |
||||
|
||||
[ObservableProperty] |
||||
private CivitModelType selectedModelType = CivitModelType.Checkpoint; |
||||
|
||||
[ObservableProperty] |
||||
private int currentPageNumber; |
||||
|
||||
[ObservableProperty] |
||||
private int totalPages; |
||||
|
||||
[ObservableProperty] |
||||
private bool hasSearched; |
||||
|
||||
[ObservableProperty] |
||||
private bool canGoToNextPage; |
||||
|
||||
[ObservableProperty] |
||||
private bool canGoToPreviousPage; |
||||
|
||||
[ObservableProperty] |
||||
private bool canGoToFirstPage; |
||||
|
||||
[ObservableProperty] |
||||
private bool canGoToLastPage; |
||||
|
||||
[ObservableProperty] |
||||
private bool isIndeterminate; |
||||
|
||||
[ObservableProperty] |
||||
private bool noResultsFound; |
||||
|
||||
[ObservableProperty] |
||||
private string noResultsText = string.Empty; |
||||
|
||||
[ObservableProperty] |
||||
private string selectedBaseModelType = "All"; |
||||
|
||||
private List<CheckpointBrowserCardViewModel> allModelCards = new(); |
||||
|
||||
public IEnumerable<CivitPeriod> AllCivitPeriods => Enum.GetValues(typeof(CivitPeriod)).Cast<CivitPeriod>(); |
||||
public IEnumerable<CivitSortMode> AllSortModes => Enum.GetValues(typeof(CivitSortMode)).Cast<CivitSortMode>(); |
||||
|
||||
public IEnumerable<CivitModelType> AllModelTypes => |
||||
Enum.GetValues(typeof(CivitModelType)) |
||||
.Cast<CivitModelType>() |
||||
.Where(t => t == CivitModelType.All || t.ConvertTo<SharedFolderType>() > 0) |
||||
.OrderBy(t => t.ToString()); |
||||
|
||||
public List<string> BaseModelOptions => new() { "All", "SD 1.5", "SD 2.1", "SDXL 0.9", "SDXL 1.0" }; |
||||
|
||||
public CivitAiBrowserViewModel( |
||||
ICivitApi civitApi, |
||||
IDownloadService downloadService, |
||||
ISettingsManager settingsManager, |
||||
ServiceManager<ViewModelBase> dialogFactory, |
||||
ILiteDbContext liteDbContext, |
||||
INotificationService notificationService |
||||
) |
||||
{ |
||||
this.civitApi = civitApi; |
||||
this.downloadService = downloadService; |
||||
this.settingsManager = settingsManager; |
||||
this.dialogFactory = dialogFactory; |
||||
this.liteDbContext = liteDbContext; |
||||
this.notificationService = notificationService; |
||||
|
||||
CurrentPageNumber = 1; |
||||
CanGoToNextPage = true; |
||||
CanGoToLastPage = true; |
||||
|
||||
Observable |
||||
.FromEventPattern<PropertyChangedEventArgs>(this, nameof(PropertyChanged)) |
||||
.Where(x => x.EventArgs.PropertyName == nameof(CurrentPageNumber)) |
||||
.Throttle(TimeSpan.FromMilliseconds(250)) |
||||
.Select<EventPattern<PropertyChangedEventArgs>, int>(_ => CurrentPageNumber) |
||||
.Where(page => page <= TotalPages && page > 0) |
||||
.ObserveOn(SynchronizationContext.Current) |
||||
.Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err)); |
||||
} |
||||
|
||||
public override void OnLoaded() |
||||
{ |
||||
if (Design.IsDesignMode) |
||||
return; |
||||
|
||||
var searchOptions = settingsManager.Settings.ModelSearchOptions; |
||||
|
||||
// Fix SelectedModelType if someone had selected the obsolete "Model" option |
||||
if (searchOptions is { SelectedModelType: CivitModelType.Model }) |
||||
{ |
||||
settingsManager.Transaction( |
||||
s => |
||||
s.ModelSearchOptions = new ModelSearchOptions( |
||||
SelectedPeriod, |
||||
SortMode, |
||||
CivitModelType.Checkpoint, |
||||
SelectedBaseModelType |
||||
) |
||||
); |
||||
searchOptions = settingsManager.Settings.ModelSearchOptions; |
||||
} |
||||
|
||||
SelectedPeriod = searchOptions?.SelectedPeriod ?? CivitPeriod.Month; |
||||
SortMode = searchOptions?.SortMode ?? CivitSortMode.HighestRated; |
||||
SelectedModelType = searchOptions?.SelectedModelType ?? CivitModelType.Checkpoint; |
||||
SelectedBaseModelType = searchOptions?.SelectedBaseModelType ?? "All"; |
||||
|
||||
ShowNsfw = settingsManager.Settings.ModelBrowserNsfwEnabled; |
||||
|
||||
settingsManager.RelayPropertyFor(this, model => model.ShowNsfw, settings => settings.ModelBrowserNsfwEnabled); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Filter predicate for model cards |
||||
/// </summary> |
||||
private bool FilterModelCardsPredicate(object? item) |
||||
{ |
||||
if (item is not CheckpointBrowserCardViewModel card) |
||||
return false; |
||||
return !card.CivitModel.Nsfw || ShowNsfw; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Background update task |
||||
/// </summary> |
||||
private async Task CivitModelQuery(CivitModelsRequest request) |
||||
{ |
||||
var timer = Stopwatch.StartNew(); |
||||
var queryText = request.Query; |
||||
try |
||||
{ |
||||
var modelsResponse = await civitApi.GetModels(request); |
||||
var models = modelsResponse.Items; |
||||
if (models is null) |
||||
{ |
||||
Logger.Debug( |
||||
"CivitAI Query {Text} returned no results (in {Elapsed:F1} s)", |
||||
queryText, |
||||
timer.Elapsed.TotalSeconds |
||||
); |
||||
return; |
||||
} |
||||
|
||||
Logger.Debug( |
||||
"CivitAI Query {Text} returned {Results} results (in {Elapsed:F1} s)", |
||||
queryText, |
||||
models.Count, |
||||
timer.Elapsed.TotalSeconds |
||||
); |
||||
|
||||
var unknown = models.Where(m => m.Type == CivitModelType.Unknown).ToList(); |
||||
if (unknown.Any()) |
||||
{ |
||||
var names = unknown.Select(m => m.Name).ToList(); |
||||
Logger.Warn("Excluded {Unknown} unknown model types: {Models}", unknown.Count, names); |
||||
} |
||||
|
||||
// Filter out unknown model types and archived/taken-down models |
||||
models = models.Where(m => m.Type.ConvertTo<SharedFolderType>() > 0).Where(m => m.Mode == null).ToList(); |
||||
|
||||
// Database update calls will invoke `OnModelsUpdated` |
||||
// Add to database |
||||
await liteDbContext.UpsertCivitModelAsync(models); |
||||
// Add as cache entry |
||||
var cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync( |
||||
new() |
||||
{ |
||||
Id = ObjectHash.GetMd5Guid(request), |
||||
InsertedAt = DateTimeOffset.UtcNow, |
||||
Request = request, |
||||
Items = models, |
||||
Metadata = modelsResponse.Metadata |
||||
} |
||||
); |
||||
|
||||
if (cacheNew) |
||||
{ |
||||
Logger.Debug("New cache entry, updating model cards"); |
||||
UpdateModelCards(models, modelsResponse.Metadata); |
||||
} |
||||
else |
||||
{ |
||||
Logger.Debug("Cache entry already exists, not updating model cards"); |
||||
} |
||||
} |
||||
catch (OperationCanceledException) |
||||
{ |
||||
notificationService.Show( |
||||
new Notification("Request to CivitAI timed out", "Please try again in a few minutes") |
||||
); |
||||
Logger.Warn($"CivitAI query timed out ({request})"); |
||||
} |
||||
catch (HttpRequestException e) |
||||
{ |
||||
notificationService.Show( |
||||
new Notification("CivitAI can't be reached right now", "Please try again in a few minutes") |
||||
); |
||||
Logger.Warn(e, $"CivitAI query HttpRequestException ({request})"); |
||||
} |
||||
catch (ApiException e) |
||||
{ |
||||
notificationService.Show( |
||||
new Notification("CivitAI can't be reached right now", "Please try again in a few minutes") |
||||
); |
||||
Logger.Warn(e, $"CivitAI query ApiException ({request})"); |
||||
} |
||||
catch (Exception e) |
||||
{ |
||||
notificationService.Show( |
||||
new Notification( |
||||
"CivitAI can't be reached right now", |
||||
$"Unknown exception during CivitAI query: {e.GetType().Name}" |
||||
) |
||||
); |
||||
Logger.Error(e, $"CivitAI query unknown exception ({request})"); |
||||
} |
||||
finally |
||||
{ |
||||
ShowMainLoadingSpinner = false; |
||||
UpdateResultsText(); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Updates model cards using api response object. |
||||
/// </summary> |
||||
private void UpdateModelCards(IEnumerable<CivitModel>? models, CivitMetadata? metadata) |
||||
{ |
||||
if (models is null) |
||||
{ |
||||
ModelCards?.Clear(); |
||||
} |
||||
else |
||||
{ |
||||
var updateCards = models |
||||
.Select(model => |
||||
{ |
||||
var cachedViewModel = cache.Get(model.Id); |
||||
if (cachedViewModel != null) |
||||
{ |
||||
if (!cachedViewModel.IsImporting) |
||||
{ |
||||
cache.Remove(model.Id); |
||||
} |
||||
|
||||
return cachedViewModel; |
||||
} |
||||
|
||||
var newCard = dialogFactory.Get<CheckpointBrowserCardViewModel>(vm => |
||||
{ |
||||
vm.CivitModel = model; |
||||
vm.OnDownloadStart = viewModel => |
||||
{ |
||||
if (cache.Get(viewModel.CivitModel.Id) != null) |
||||
return; |
||||
cache.Add(viewModel.CivitModel.Id, viewModel); |
||||
}; |
||||
|
||||
return vm; |
||||
}); |
||||
|
||||
return newCard; |
||||
}) |
||||
.ToList(); |
||||
|
||||
allModelCards = updateCards; |
||||
|
||||
var filteredCards = updateCards.Where(FilterModelCardsPredicate); |
||||
if (SortMode == CivitSortMode.Installed) |
||||
{ |
||||
filteredCards = filteredCards.OrderByDescending(x => x.UpdateCardText == "Update Available"); |
||||
} |
||||
|
||||
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(filteredCards); |
||||
} |
||||
TotalPages = metadata?.TotalPages ?? 1; |
||||
CanGoToFirstPage = CurrentPageNumber != 1; |
||||
CanGoToPreviousPage = CurrentPageNumber > 1; |
||||
CanGoToNextPage = CurrentPageNumber < TotalPages; |
||||
CanGoToLastPage = CurrentPageNumber != TotalPages; |
||||
// Status update |
||||
ShowMainLoadingSpinner = false; |
||||
IsIndeterminate = false; |
||||
HasSearched = true; |
||||
} |
||||
|
||||
private string previousSearchQuery = string.Empty; |
||||
|
||||
[RelayCommand] |
||||
private async Task SearchModels() |
||||
{ |
||||
var timer = Stopwatch.StartNew(); |
||||
|
||||
if (SearchQuery != previousSearchQuery) |
||||
{ |
||||
// Reset page number |
||||
CurrentPageNumber = 1; |
||||
previousSearchQuery = SearchQuery; |
||||
} |
||||
|
||||
// Build request |
||||
var modelRequest = new CivitModelsRequest |
||||
{ |
||||
Limit = MaxModelsPerPage, |
||||
Nsfw = "true", // Handled by local view filter |
||||
Sort = SortMode, |
||||
Period = SelectedPeriod, |
||||
Page = CurrentPageNumber |
||||
}; |
||||
|
||||
if (SearchQuery.StartsWith("#")) |
||||
{ |
||||
modelRequest.Tag = SearchQuery[1..]; |
||||
} |
||||
else if (SearchQuery.StartsWith("@")) |
||||
{ |
||||
modelRequest.Username = SearchQuery[1..]; |
||||
} |
||||
else |
||||
{ |
||||
modelRequest.Query = SearchQuery; |
||||
} |
||||
|
||||
if (SelectedModelType != CivitModelType.All) |
||||
{ |
||||
modelRequest.Types = new[] { SelectedModelType }; |
||||
} |
||||
|
||||
if (SelectedBaseModelType != "All") |
||||
{ |
||||
modelRequest.BaseModel = SelectedBaseModelType; |
||||
} |
||||
|
||||
if (SortMode == CivitSortMode.Installed) |
||||
{ |
||||
var connectedModels = CheckpointFile |
||||
.GetAllCheckpointFiles(settingsManager.ModelsDirectory) |
||||
.Where(c => c.IsConnectedModel); |
||||
|
||||
modelRequest.CommaSeparatedModelIds = string.Join( |
||||
",", |
||||
connectedModels.Select(c => c.ConnectedModel!.ModelId).GroupBy(m => m).Select(g => g.First()) |
||||
); |
||||
modelRequest.Sort = null; |
||||
modelRequest.Period = null; |
||||
} |
||||
else if (SortMode == CivitSortMode.Favorites) |
||||
{ |
||||
var favoriteModels = settingsManager.Settings.FavoriteModels; |
||||
|
||||
if (!favoriteModels.Any()) |
||||
{ |
||||
notificationService.Show( |
||||
"No Favorites", |
||||
"You have not added any models to your Favorites.", |
||||
NotificationType.Error |
||||
); |
||||
return; |
||||
} |
||||
|
||||
modelRequest.CommaSeparatedModelIds = string.Join(",", favoriteModels); |
||||
modelRequest.Sort = null; |
||||
modelRequest.Period = null; |
||||
} |
||||
|
||||
// See if query is cached |
||||
CivitModelQueryCacheEntry? cachedQuery = null; |
||||
|
||||
try |
||||
{ |
||||
cachedQuery = await liteDbContext |
||||
.CivitModelQueryCache |
||||
.IncludeAll() |
||||
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest)); |
||||
} |
||||
catch (Exception e) |
||||
{ |
||||
// Suppress 'Training_Data' enum not found exceptions |
||||
// Caused by enum name change |
||||
// Ignore to do a new search to overwrite the cache |
||||
if ( |
||||
!( |
||||
e is LiteException or LiteAsyncException |
||||
&& e.InnerException is ArgumentException inner |
||||
&& inner.Message.Contains("Training_Data") |
||||
) |
||||
) |
||||
{ |
||||
// Otherwise log error |
||||
Logger.Error(e, "Error while querying CivitModelQueryCache"); |
||||
} |
||||
} |
||||
|
||||
// If cached, update model cards |
||||
if (cachedQuery is not null) |
||||
{ |
||||
var elapsed = timer.Elapsed; |
||||
Logger.Debug( |
||||
"Using cached query for {Text} [{RequestHash}] (in {Elapsed:F1} s)", |
||||
SearchQuery, |
||||
modelRequest.GetHashCode(), |
||||
elapsed.TotalSeconds |
||||
); |
||||
UpdateModelCards(cachedQuery.Items, cachedQuery.Metadata); |
||||
|
||||
// Start remote query (background mode) |
||||
// Skip when last query was less than 2 min ago |
||||
var timeSinceCache = DateTimeOffset.UtcNow - cachedQuery.InsertedAt; |
||||
if (timeSinceCache?.TotalMinutes >= 2) |
||||
{ |
||||
CivitModelQuery(modelRequest).SafeFireAndForget(); |
||||
Logger.Debug( |
||||
"Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query", |
||||
timeSinceCache.Value.TotalSeconds |
||||
); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
// Not cached, wait for remote query |
||||
ShowMainLoadingSpinner = true; |
||||
await CivitModelQuery(modelRequest); |
||||
} |
||||
|
||||
UpdateResultsText(); |
||||
} |
||||
|
||||
public void FirstPage() |
||||
{ |
||||
CurrentPageNumber = 1; |
||||
} |
||||
|
||||
public void PreviousPage() |
||||
{ |
||||
if (CurrentPageNumber == 1) |
||||
return; |
||||
|
||||
CurrentPageNumber--; |
||||
} |
||||
|
||||
public void NextPage() |
||||
{ |
||||
if (CurrentPageNumber == TotalPages) |
||||
return; |
||||
|
||||
CurrentPageNumber++; |
||||
} |
||||
|
||||
public void LastPage() |
||||
{ |
||||
CurrentPageNumber = TotalPages; |
||||
} |
||||
|
||||
public void ClearSearchQuery() |
||||
{ |
||||
SearchQuery = string.Empty; |
||||
} |
||||
|
||||
partial void OnShowNsfwChanged(bool value) |
||||
{ |
||||
settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled, value); |
||||
// ModelCardsView?.Refresh(); |
||||
var updateCards = allModelCards.Where(FilterModelCardsPredicate); |
||||
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards); |
||||
|
||||
if (!HasSearched) |
||||
return; |
||||
|
||||
UpdateResultsText(); |
||||
} |
||||
|
||||
partial void OnSelectedPeriodChanged(CivitPeriod value) |
||||
{ |
||||
TrySearchAgain().SafeFireAndForget(); |
||||
settingsManager.Transaction( |
||||
s => |
||||
s.ModelSearchOptions = new ModelSearchOptions(value, SortMode, SelectedModelType, SelectedBaseModelType) |
||||
); |
||||
} |
||||
|
||||
partial void OnSortModeChanged(CivitSortMode value) |
||||
{ |
||||
TrySearchAgain().SafeFireAndForget(); |
||||
settingsManager.Transaction( |
||||
s => |
||||
s.ModelSearchOptions = new ModelSearchOptions( |
||||
SelectedPeriod, |
||||
value, |
||||
SelectedModelType, |
||||
SelectedBaseModelType |
||||
) |
||||
); |
||||
} |
||||
|
||||
partial void OnSelectedModelTypeChanged(CivitModelType value) |
||||
{ |
||||
TrySearchAgain().SafeFireAndForget(); |
||||
settingsManager.Transaction( |
||||
s => s.ModelSearchOptions = new ModelSearchOptions(SelectedPeriod, SortMode, value, SelectedBaseModelType) |
||||
); |
||||
} |
||||
|
||||
partial void OnSelectedBaseModelTypeChanged(string value) |
||||
{ |
||||
TrySearchAgain().SafeFireAndForget(); |
||||
settingsManager.Transaction( |
||||
s => s.ModelSearchOptions = new ModelSearchOptions(SelectedPeriod, SortMode, SelectedModelType, value) |
||||
); |
||||
} |
||||
|
||||
private async Task TrySearchAgain(bool shouldUpdatePageNumber = true) |
||||
{ |
||||
if (!HasSearched) |
||||
return; |
||||
ModelCards?.Clear(); |
||||
|
||||
if (shouldUpdatePageNumber) |
||||
{ |
||||
CurrentPageNumber = 1; |
||||
} |
||||
|
||||
// execute command instead of calling method directly so that the IsRunning property gets updated |
||||
await SearchModelsCommand.ExecuteAsync(null); |
||||
} |
||||
|
||||
private void UpdateResultsText() |
||||
{ |
||||
NoResultsFound = ModelCards?.Count <= 0; |
||||
NoResultsText = |
||||
allModelCards.Count > 0 ? $"{allModelCards.Count} results hidden by filters" : "No results found"; |
||||
} |
||||
|
||||
public override string Header => Resources.Label_CivitAi; |
||||
} |
@ -0,0 +1,193 @@
|
||||
using System; |
||||
using System.Collections.Concurrent; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Reactive.Linq; |
||||
using System.Text.Json; |
||||
using System.Text.Json.Serialization; |
||||
using System.Threading.Tasks; |
||||
using Avalonia; |
||||
using Avalonia.Controls.Notifications; |
||||
using Avalonia.Data; |
||||
using Avalonia.Input; |
||||
using Avalonia.Markup.Xaml.MarkupExtensions; |
||||
using Avalonia.Media; |
||||
using Avalonia.Threading; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using DynamicData; |
||||
using DynamicData.Binding; |
||||
using FluentAvalonia.UI.Controls; |
||||
using StabilityMatrix.Avalonia.Languages; |
||||
using StabilityMatrix.Avalonia.Models.HuggingFace; |
||||
using StabilityMatrix.Avalonia.Services; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Avalonia.ViewModels.HuggingFacePage; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Extensions; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; |
||||
|
||||
[View(typeof(Views.HuggingFacePage))] |
||||
[Singleton] |
||||
public partial class HuggingFacePageViewModel : TabViewModelBase |
||||
{ |
||||
private readonly ITrackedDownloadService trackedDownloadService; |
||||
private readonly ISettingsManager settingsManager; |
||||
private readonly INotificationService notificationService; |
||||
|
||||
public SourceCache<HuggingfaceItem, string> ItemsCache { get; } = new(i => i.RepositoryPath + i.ModelName); |
||||
|
||||
public IObservableCollection<CategoryViewModel> Categories { get; set; } = |
||||
new ObservableCollectionExtended<CategoryViewModel>(); |
||||
|
||||
public string DownloadPercentText => |
||||
Math.Abs(TotalProgress.Percentage - 100f) < 0.001f |
||||
? "Download Complete" |
||||
: $"Downloading {TotalProgress.Percentage:0}%"; |
||||
|
||||
[ObservableProperty] |
||||
private int numSelected; |
||||
|
||||
private ConcurrentDictionary<Guid, ProgressReport> progressReports = new(); |
||||
|
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(DownloadPercentText))] |
||||
private ProgressReport totalProgress; |
||||
|
||||
private readonly DispatcherTimer progressTimer = new() { Interval = TimeSpan.FromMilliseconds(100) }; |
||||
|
||||
public HuggingFacePageViewModel( |
||||
ITrackedDownloadService trackedDownloadService, |
||||
ISettingsManager settingsManager, |
||||
INotificationService notificationService |
||||
) |
||||
{ |
||||
this.trackedDownloadService = trackedDownloadService; |
||||
this.settingsManager = settingsManager; |
||||
this.notificationService = notificationService; |
||||
|
||||
ItemsCache |
||||
.Connect() |
||||
.DeferUntilLoaded() |
||||
.Group(i => i.ModelCategory) |
||||
.Transform(g => new CategoryViewModel(g.Cache.Items) { Title = g.Key.GetDescription() ?? g.Key.ToString() }) |
||||
.SortBy(vm => vm.Title) |
||||
.Bind(Categories) |
||||
.WhenAnyPropertyChanged() |
||||
.Subscribe(_ => NumSelected = Categories.Sum(c => c.NumSelected)); |
||||
|
||||
progressTimer.Tick += (_, _) => |
||||
{ |
||||
var currentSum = 0ul; |
||||
var totalSum = 0ul; |
||||
foreach (var progress in progressReports.Values) |
||||
{ |
||||
currentSum += progress.Current ?? 0; |
||||
totalSum += progress.Total ?? 0; |
||||
} |
||||
|
||||
TotalProgress = new ProgressReport(current: currentSum, total: totalSum); |
||||
}; |
||||
} |
||||
|
||||
public override void OnLoaded() |
||||
{ |
||||
if (ItemsCache.Count > 0) |
||||
return; |
||||
|
||||
using var reader = new StreamReader(Assets.HfPackagesJson.Open()); |
||||
var packages = |
||||
JsonSerializer.Deserialize<IReadOnlyList<HuggingfaceItem>>( |
||||
reader.ReadToEnd(), |
||||
new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } } |
||||
) ?? throw new InvalidOperationException("Failed to read hf-packages.json"); |
||||
|
||||
ItemsCache.EditDiff(packages, (a, b) => a.RepositoryPath == b.RepositoryPath); |
||||
} |
||||
|
||||
public void ClearSelection() |
||||
{ |
||||
foreach (var category in Categories) |
||||
{ |
||||
category.IsChecked = true; |
||||
category.IsChecked = false; |
||||
} |
||||
} |
||||
|
||||
public void SelectAll() |
||||
{ |
||||
foreach (var category in Categories) |
||||
{ |
||||
category.IsChecked = true; |
||||
} |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task ImportSelected() |
||||
{ |
||||
var selected = Categories.SelectMany(c => c.Items).Where(i => i.IsSelected).ToArray(); |
||||
|
||||
foreach (var viewModel in selected) |
||||
{ |
||||
foreach (var file in viewModel.Item.Files) |
||||
{ |
||||
var url = $"https://huggingface.co/{viewModel.Item.RepositoryPath}/resolve/main/{file}?download=true"; |
||||
var sharedFolderType = viewModel.Item.ModelCategory.ConvertTo<SharedFolderType>(); |
||||
var downloadPath = new FilePath( |
||||
Path.Combine( |
||||
settingsManager.ModelsDirectory, |
||||
sharedFolderType.GetDescription() ?? sharedFolderType.ToString(), |
||||
viewModel.Item.Subfolder ?? string.Empty, |
||||
file |
||||
) |
||||
); |
||||
Directory.CreateDirectory(downloadPath.Directory); |
||||
var download = trackedDownloadService.NewDownload(url, downloadPath); |
||||
download.ProgressUpdate += DownloadOnProgressUpdate; |
||||
download.Start(); |
||||
|
||||
await Task.Delay(Random.Shared.Next(50, 100)); |
||||
} |
||||
} |
||||
progressTimer.Start(); |
||||
} |
||||
|
||||
private void DownloadOnProgressUpdate(object? sender, ProgressReport e) |
||||
{ |
||||
if (sender is not TrackedDownload trackedDownload) |
||||
return; |
||||
|
||||
progressReports[trackedDownload.Id] = e; |
||||
} |
||||
|
||||
partial void OnTotalProgressChanged(ProgressReport value) |
||||
{ |
||||
if (Math.Abs(value.Percentage - 100) < 0.001f) |
||||
{ |
||||
notificationService.Show( |
||||
"Download complete", |
||||
"All selected models have been downloaded.", |
||||
NotificationType.Success |
||||
); |
||||
progressTimer.Stop(); |
||||
DelayedClearProgress(TimeSpan.FromSeconds(1.5)); |
||||
} |
||||
} |
||||
|
||||
private void DelayedClearProgress(TimeSpan delay) |
||||
{ |
||||
Task.Delay(delay) |
||||
.ContinueWith(_ => |
||||
{ |
||||
TotalProgress = new ProgressReport(0, 0); |
||||
}); |
||||
} |
||||
|
||||
public override string Header => Resources.Label_HuggingFace; |
||||
} |
@ -0,0 +1,47 @@
|
||||
using System.Collections.Generic; |
||||
using System.ComponentModel; |
||||
using Avalonia; |
||||
using Avalonia.PropertyGrid.ViewModels; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using OneOf; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.Languages; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Avalonia.Views.Dialogs; |
||||
using StabilityMatrix.Core.Attributes; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; |
||||
|
||||
[View(typeof(PropertyGridDialog))] |
||||
[ManagedService] |
||||
[Transient] |
||||
public partial class PropertyGridViewModel : ContentDialogViewModelBase |
||||
{ |
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(SelectedObjectItemsSource))] |
||||
private OneOf<INotifyPropertyChanged, IEnumerable<INotifyPropertyChanged>>? selectedObject; |
||||
|
||||
public IEnumerable<INotifyPropertyChanged>? SelectedObjectItemsSource => |
||||
SelectedObject?.Match(single => [single], multiple => multiple); |
||||
|
||||
[ObservableProperty] |
||||
private PropertyGridShowStyle showStyle = PropertyGridShowStyle.Alphabetic; |
||||
|
||||
[ObservableProperty] |
||||
private IReadOnlyList<string>? excludeCategories; |
||||
|
||||
[ObservableProperty] |
||||
private IReadOnlyList<string>? includeCategories; |
||||
|
||||
/// <inheritdoc /> |
||||
public override BetterContentDialog GetDialog() |
||||
{ |
||||
var dialog = base.GetDialog(); |
||||
|
||||
dialog.Padding = new Thickness(0); |
||||
dialog.CloseOnClickOutside = true; |
||||
dialog.CloseButtonText = Resources.Action_Close; |
||||
|
||||
return dialog; |
||||
} |
||||
} |
@ -0,0 +1,52 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using DynamicData; |
||||
using DynamicData.Binding; |
||||
using StabilityMatrix.Avalonia.Models.HuggingFace; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.HuggingFacePage; |
||||
|
||||
public partial class CategoryViewModel : ViewModelBase |
||||
{ |
||||
[ObservableProperty] |
||||
private IObservableCollection<HuggingfaceItemViewModel> items = |
||||
new ObservableCollectionExtended<HuggingfaceItemViewModel>(); |
||||
|
||||
public SourceCache<HuggingfaceItem, string> ItemsCache { get; } = new(i => i.RepositoryPath + i.ModelName); |
||||
|
||||
[ObservableProperty] |
||||
private string? title; |
||||
|
||||
[ObservableProperty] |
||||
private bool isChecked; |
||||
|
||||
[ObservableProperty] |
||||
private int numSelected; |
||||
|
||||
public CategoryViewModel(IEnumerable<HuggingfaceItem> items) |
||||
{ |
||||
ItemsCache |
||||
.Connect() |
||||
.DeferUntilLoaded() |
||||
.Transform(i => new HuggingfaceItemViewModel { Item = i }) |
||||
.Bind(Items) |
||||
.WhenPropertyChanged(p => p.IsSelected) |
||||
.Subscribe(_ => NumSelected = Items.Count(i => i.IsSelected)); |
||||
|
||||
ItemsCache.EditDiff(items, (a, b) => a.RepositoryPath == b.RepositoryPath); |
||||
} |
||||
|
||||
partial void OnIsCheckedChanged(bool value) |
||||
{ |
||||
if (Items is null) |
||||
return; |
||||
|
||||
foreach (var item in Items) |
||||
{ |
||||
item.IsSelected = value; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,25 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using StabilityMatrix.Avalonia.Models.HuggingFace; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.HuggingFacePage; |
||||
|
||||
public partial class HuggingfaceItemViewModel : ViewModelBase |
||||
{ |
||||
[ObservableProperty] |
||||
private HuggingfaceItem item; |
||||
|
||||
[ObservableProperty] |
||||
private bool isSelected; |
||||
|
||||
public string LicenseUrl => |
||||
$"https://huggingface.co/{Item.RepositoryPath}/blob/main/{Item.LicensePath ?? "README.md"}"; |
||||
public string RepoUrl => $"https://huggingface.co/{Item.RepositoryPath}"; |
||||
|
||||
[RelayCommand] |
||||
private void ToggleSelected() |
||||
{ |
||||
IsSelected = !IsSelected; |
||||
} |
||||
} |
@ -1,531 +1,18 @@
|
||||
<UserControl |
||||
x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage" |
||||
xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" |
||||
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" |
||||
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager" |
||||
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" |
||||
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" |
||||
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers" |
||||
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}" |
||||
d:DesignHeight="700" |
||||
d:DesignWidth="800" |
||||
x:CompileBindings="True" |
||||
x:DataType="viewModels:CheckpointBrowserViewModel" |
||||
mc:Ignorable="d"> |
||||
|
||||
<UserControl.Styles> |
||||
<Style Selector="Border#HoverBorder"> |
||||
<Setter Property="Transitions"> |
||||
<Transitions> |
||||
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.237"/> |
||||
</Transitions> |
||||
</Setter> |
||||
|
||||
<Style Selector="^ asyncImageLoader|AdvancedImage"> |
||||
<Setter Property="Transitions"> |
||||
<Transitions> |
||||
<TransformOperationsTransition Property="RenderTransform" |
||||
Duration="0:0:0.237"> |
||||
<TransformOperationsTransition.Easing> |
||||
<QuadraticEaseInOut/> |
||||
</TransformOperationsTransition.Easing> |
||||
</TransformOperationsTransition> |
||||
</Transitions> |
||||
</Setter> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Setter Property="BoxShadow" Value="0 0 40 0 #60000000"/> |
||||
<Setter Property="Cursor" Value="Hand" /> |
||||
<Style Selector="^ asyncImageLoader|AdvancedImage"> |
||||
<Setter Property="CornerRadius" Value="12"/> |
||||
<Setter Property="RenderTransform" Value="scale(1.03, 1.03)"/> |
||||
</Style> |
||||
<Style Selector="^ Border#ModelCardBottom"> |
||||
<Setter Property="Background" Value="#CC000000" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:not(:pointerover)"> |
||||
<Setter Property="BoxShadow" Value="0 0 20 0 #60000000"/> |
||||
<Setter Property="Cursor" Value="Arrow" /> |
||||
<Style Selector="^ asyncImageLoader|AdvancedImage"> |
||||
<Setter Property="CornerRadius" Value="8"/> |
||||
<Setter Property="RenderTransform" Value="scale(1, 1)"/> |
||||
</Style> |
||||
<Style Selector="^ Border#ModelCardBottom"> |
||||
<Setter Property="Background" Value="#99000000" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
</UserControl.Styles> |
||||
|
||||
<UserControl.Resources> |
||||
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/> |
||||
<DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}"> |
||||
<Border |
||||
Name="HoverBorder" |
||||
Padding="0" |
||||
BorderThickness="0" |
||||
Margin="8" |
||||
ClipToBounds="True" |
||||
CornerRadius="8"> |
||||
<Border.ContextFlyout> |
||||
<MenuFlyout> |
||||
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnCivitAi}" |
||||
Command="{Binding OpenModelCommand}"> |
||||
<MenuItem.Icon> |
||||
<ui:SymbolIcon Symbol="Open" /> |
||||
</MenuItem.Icon> |
||||
</MenuItem> |
||||
</MenuFlyout> |
||||
</Border.ContextFlyout> |
||||
<Button |
||||
Name="ModelCard" |
||||
Classes="transparent-full" |
||||
Padding="0" |
||||
BorderThickness="0" |
||||
VerticalContentAlignment="Top" |
||||
CornerRadius="8" |
||||
Command="{Binding ShowVersionDialogCommand}" |
||||
CommandParameter="{Binding CivitModel}" |
||||
IsEnabled="{Binding !IsImporting}"> |
||||
<Grid RowDefinitions="*, Auto"> |
||||
<controls:BetterAdvancedImage |
||||
Grid.RowSpan="2" |
||||
CornerRadius="8" |
||||
Width="330" |
||||
Height="400" |
||||
Source="{Binding CardImage}" |
||||
Stretch="UniformToFill" |
||||
StretchDirection="Both"/> |
||||
|
||||
<StackPanel |
||||
Grid.Row="0" |
||||
HorizontalAlignment="Right" |
||||
Orientation="Horizontal"> |
||||
<Button |
||||
Margin="0,8,8,0" |
||||
HorizontalAlignment="Right" |
||||
VerticalAlignment="Top" |
||||
Classes="transparent-info" |
||||
Command="{Binding ToggleFavoriteCommand}" |
||||
FontSize="20" |
||||
IsVisible="{Binding !IsFavorite}"> |
||||
<Grid> |
||||
<ui:SymbolIcon Symbol="StarAdd" /> |
||||
</Grid> |
||||
</Button> |
||||
<Button |
||||
Margin="0,8,8,0" |
||||
HorizontalAlignment="Right" |
||||
VerticalAlignment="Top" |
||||
Classes="success" |
||||
Command="{Binding ToggleFavoriteCommand}" |
||||
FontSize="20" |
||||
IsVisible="{Binding IsFavorite}"> |
||||
<Grid> |
||||
<ui:SymbolIcon Symbol="StarFilled" /> |
||||
</Grid> |
||||
</Button> |
||||
</StackPanel> |
||||
|
||||
<!-- Username pill card --> |
||||
<Border |
||||
BoxShadow="inset 1.2 0 80 1.8 #66000000" |
||||
CornerRadius="16" |
||||
Margin="4" |
||||
Grid.Row="0" |
||||
HorizontalAlignment="Left" |
||||
VerticalAlignment="Bottom"> |
||||
<Border.Resources> |
||||
<DropShadowEffect |
||||
x:Key="TextDropShadowEffect" |
||||
BlurRadius="12" |
||||
Color="#FF000000" |
||||
Opacity="0.9"/> |
||||
<DropShadowEffect |
||||
x:Key="ImageDropShadowEffect" |
||||
BlurRadius="12" |
||||
Color="#FF000000" |
||||
Opacity="0.2"/> |
||||
</Border.Resources> |
||||
<Button |
||||
Command="{x:Static helpers:IOCommands.OpenUrlCommand}" |
||||
CommandParameter="{Binding CivitModel.Creator.ProfileUrl}" |
||||
CornerRadius="16" |
||||
Classes="transparent" |
||||
Padding="10,4"> |
||||
<StackPanel Orientation="Horizontal" Spacing="6"> |
||||
<controls:BetterAdvancedImage |
||||
Width="22" |
||||
Height="22" |
||||
Effect="{StaticResource ImageDropShadowEffect}" |
||||
CornerRadius="11" |
||||
RenderOptions.BitmapInterpolationMode="HighQuality" |
||||
IsVisible="{Binding CivitModel.Creator.Image, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" |
||||
Source="{Binding CivitModel.Creator.Image}"/> |
||||
<TextBlock |
||||
VerticalAlignment="Center" |
||||
Effect="{StaticResource TextDropShadowEffect}" |
||||
Text="{Binding CivitModel.Creator.Username}"/> |
||||
</StackPanel> |
||||
</Button> |
||||
</Border> |
||||
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal"> |
||||
<controls:Card |
||||
Height="24" |
||||
Margin="8,8,0,0" |
||||
Padding="4" |
||||
HorizontalAlignment="Left" |
||||
VerticalAlignment="Top" |
||||
Classes="info"> |
||||
|
||||
<TextBlock |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
FontSize="11" |
||||
FontWeight="Medium" |
||||
Text="{Binding CivitModel.Type}" /> |
||||
</controls:Card> |
||||
<controls:Card |
||||
Height="24" |
||||
Margin="4,8,0,0" |
||||
Padding="4" |
||||
HorizontalAlignment="Left" |
||||
VerticalAlignment="Top" |
||||
Classes="info"> |
||||
|
||||
<TextBlock |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
FontSize="11" |
||||
FontWeight="Medium" |
||||
Text="{Binding CivitModel.BaseModelType}" /> |
||||
</controls:Card> |
||||
<controls:Card |
||||
Height="24" |
||||
Margin="4,8,0,0" |
||||
Padding="4" |
||||
HorizontalAlignment="Left" |
||||
VerticalAlignment="Top" |
||||
Classes="success" |
||||
IsVisible="{Binding ShowUpdateCard}"> |
||||
|
||||
<TextBlock |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
FontSize="11" |
||||
FontWeight="Medium" |
||||
Text="{Binding UpdateCardText}" /> |
||||
</controls:Card> |
||||
</StackPanel> |
||||
<Border |
||||
Grid.Row="0" |
||||
Grid.RowSpan="2" |
||||
Margin="0,0,0,0" |
||||
HorizontalAlignment="Stretch" |
||||
VerticalAlignment="Stretch" |
||||
Background="#DD000000" |
||||
CornerRadius="8" |
||||
IsVisible="{Binding IsImporting}" |
||||
ZIndex="1" /> |
||||
<StackPanel |
||||
Grid.Row="0" |
||||
Grid.RowSpan="2" |
||||
HorizontalAlignment="Stretch" |
||||
VerticalAlignment="Center" |
||||
IsVisible="{Binding IsImporting}" |
||||
Orientation="Vertical" |
||||
ZIndex="2"> |
||||
<controls:ProgressRing |
||||
Width="120" |
||||
Height="120" |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
EndAngle="450" |
||||
IsIndeterminate="False" |
||||
StartAngle="90" |
||||
Value="{Binding Value}" /> |
||||
<TextBlock |
||||
Margin="0,8,0,0" |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
Text="{Binding Text, TargetNullValue=Importing...}" /> |
||||
</StackPanel> |
||||
|
||||
<Border |
||||
Name="ModelCardBottom" |
||||
Grid.Row="1"> |
||||
<Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto, Auto, Auto"> |
||||
|
||||
<!-- |
||||
TextTrimming causing issues with unicode chars until |
||||
https://github.com/AvaloniaUI/Avalonia/pull/13385 is released |
||||
--> |
||||
<TextBlock |
||||
Grid.ColumnSpan="2" |
||||
MaxWidth="250" |
||||
Margin="8,0,8,0" |
||||
HorizontalAlignment="Left" |
||||
VerticalAlignment="Center" |
||||
FontWeight="SemiBold" |
||||
Foreground="{DynamicResource TextControlForeground}" |
||||
LetterSpacing="0.33" |
||||
Text="{Binding CivitModel.Name}" |
||||
TextWrapping="NoWrap" |
||||
ToolTip.Tip="{Binding CivitModel.Name}" /> |
||||
|
||||
<TextBlock |
||||
Grid.Row="1" |
||||
Grid.Column="0" |
||||
Margin="8,-4,0,0" |
||||
VerticalAlignment="Center" |
||||
FontSize="11" |
||||
Foreground="{DynamicResource TextControlForeground}" |
||||
Text="{Binding CivitModel.LatestModelVersionName, FallbackValue=''}" /> |
||||
|
||||
<StackPanel |
||||
Grid.Row="2" |
||||
Grid.Column="0" |
||||
Orientation="Horizontal"> |
||||
|
||||
<controls:StarsRating |
||||
Margin="8,8,0,8" |
||||
Background="#66000000" |
||||
FontSize="16" |
||||
Foreground="{DynamicResource ThemeEldenRingOrangeColor}" |
||||
Value="{Binding CivitModel.Stats.Rating}" /> |
||||
<TextBlock |
||||
Margin="4,0,0,0" |
||||
VerticalAlignment="Center" |
||||
Text="{Binding CivitModel.Stats.RatingCount}" |
||||
TextAlignment="Center" /> |
||||
</StackPanel> |
||||
|
||||
<StackPanel |
||||
Grid.Row="2" |
||||
Grid.Column="1" |
||||
HorizontalAlignment="Right" |
||||
Orientation="Horizontal"> |
||||
<avalonia:Icon Value="fa-solid fa-heart" /> |
||||
<TextBlock |
||||
Margin="4,0" |
||||
VerticalAlignment="Center" |
||||
Text="{Binding CivitModel.Stats.FavoriteCount, Converter={StaticResource KiloFormatterConverter}}" /> |
||||
|
||||
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" /> |
||||
<TextBlock |
||||
Margin="0,0,4,0" |
||||
VerticalAlignment="Center" |
||||
Text="{Binding CivitModel.Stats.DownloadCount, Converter={StaticResource KiloFormatterConverter}}" /> |
||||
</StackPanel> |
||||
<Button |
||||
Grid.Row="0" |
||||
Grid.Column="1" |
||||
Width="32" |
||||
Margin="0,4,4,0" |
||||
HorizontalAlignment="Right" |
||||
VerticalAlignment="Top" |
||||
HorizontalContentAlignment="Right" |
||||
VerticalContentAlignment="Top" |
||||
BorderThickness="0" |
||||
Classes="transparent"> |
||||
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" /> |
||||
<Button.Flyout> |
||||
<MenuFlyout> |
||||
<MenuItem Command="{Binding OpenModelCommand}" Header="{x:Static lang:Resources.Action_OpenOnCivitAi}"> |
||||
<MenuItem.Icon> |
||||
<ui:SymbolIcon Symbol="Open" /> |
||||
</MenuItem.Icon> |
||||
</MenuItem> |
||||
</MenuFlyout> |
||||
</Button.Flyout> |
||||
</Button> |
||||
</Grid> |
||||
</Border> |
||||
</Grid> |
||||
</Button> |
||||
</Border> |
||||
|
||||
</DataTemplate> |
||||
</UserControl.Resources> |
||||
|
||||
<Grid Margin="0,8,0,0" RowDefinitions="Auto,*,Auto"> |
||||
|
||||
<StackPanel Margin="8" Orientation="Vertical"> |
||||
<Grid ColumnDefinitions="*,Auto"> |
||||
|
||||
<TextBox |
||||
Margin="8,0,0,0" |
||||
HorizontalAlignment="Stretch" |
||||
KeyDown="InputElement_OnKeyDown" |
||||
Text="{Binding SearchQuery, Mode=TwoWay}" |
||||
Watermark="{x:Static lang:Resources.Label_ModelSearchWatermark}"> |
||||
<TextBox.InnerRightContent> |
||||
<Button |
||||
Classes="transparent-full" |
||||
Command="{Binding ClearSearchQuery}" |
||||
IsVisible="{Binding SearchQuery.Length}"> |
||||
<ui:SymbolIcon Symbol="Cancel" /> |
||||
</Button> |
||||
</TextBox.InnerRightContent> |
||||
</TextBox> |
||||
|
||||
<Button |
||||
Grid.Column="1" |
||||
Width="80" |
||||
Margin="8,0,8,0" |
||||
VerticalAlignment="Stretch" |
||||
Classes="accent" |
||||
Command="{Binding SearchModelsCommand}" |
||||
IsDefault="True"> |
||||
<Grid> |
||||
<controls:ProgressRing |
||||
MinWidth="16" |
||||
MinHeight="16" |
||||
VerticalAlignment="Center" |
||||
BorderThickness="4" |
||||
IsIndeterminate="True" |
||||
IsVisible="{Binding SearchModelsCommand.IsRunning}" /> |
||||
<TextBlock |
||||
VerticalAlignment="Center" |
||||
IsVisible="{Binding !SearchModelsCommand.IsRunning}" |
||||
Text="{x:Static lang:Resources.Action_Search}" /> |
||||
</Grid> |
||||
</Button> |
||||
</Grid> |
||||
<DockPanel> |
||||
<StackPanel Margin="8,8,4,8" Orientation="Vertical"> |
||||
<Label Content="{x:Static lang:Resources.Label_Sort}" /> |
||||
<ComboBox |
||||
MinWidth="100" |
||||
ItemsSource="{Binding AllSortModes}" |
||||
SelectedItem="{Binding SortMode}" /> |
||||
</StackPanel> |
||||
|
||||
<StackPanel Margin="4,8" Orientation="Vertical"> |
||||
<Label Content="{x:Static lang:Resources.Label_TimePeriod}" /> |
||||
<ComboBox |
||||
MinWidth="100" |
||||
ItemsSource="{Binding AllCivitPeriods}" |
||||
SelectedItem="{Binding SelectedPeriod}" /> |
||||
</StackPanel> |
||||
|
||||
<StackPanel Margin="4,8" Orientation="Vertical"> |
||||
<Label Content="{x:Static lang:Resources.Label_ModelType}" /> |
||||
<ComboBox |
||||
MinWidth="100" |
||||
ItemsSource="{Binding AllModelTypes}" |
||||
SelectedItem="{Binding SelectedModelType}" /> |
||||
</StackPanel> |
||||
|
||||
<StackPanel Margin="4,8" Orientation="Vertical"> |
||||
<Label Content="{x:Static lang:Resources.Label_BaseModel}" /> |
||||
<ComboBox |
||||
MinWidth="100" |
||||
ItemsSource="{Binding BaseModelOptions}" |
||||
SelectedItem="{Binding SelectedBaseModelType}" /> |
||||
</StackPanel> |
||||
|
||||
|
||||
<CheckBox |
||||
Margin="8,8,8,0" |
||||
HorizontalAlignment="Right" |
||||
Content="{x:Static lang:Resources.Label_ShowNsfwContent}" |
||||
IsChecked="{Binding ShowNsfw, Mode=TwoWay}" /> |
||||
</DockPanel> |
||||
|
||||
</StackPanel> |
||||
|
||||
<ScrollViewer |
||||
Grid.Row="1" |
||||
Margin="8,0,8,0" |
||||
ScrollChanged="ScrollViewer_OnScrollChanged"> |
||||
<ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}" |
||||
HorizontalAlignment="Center" |
||||
ItemsSource="{Binding ModelCards}"> |
||||
<ItemsRepeater.Layout> |
||||
<UniformGridLayout Orientation="Horizontal" /> |
||||
</ItemsRepeater.Layout> |
||||
</ItemsRepeater> |
||||
</ScrollViewer> |
||||
|
||||
<TextBlock |
||||
Grid.Row="2" |
||||
Margin="16,8" |
||||
VerticalAlignment="Bottom" |
||||
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}" /> |
||||
|
||||
<StackPanel |
||||
Grid.Row="2" |
||||
Margin="8" |
||||
HorizontalAlignment="Center" |
||||
IsVisible="{Binding HasSearched}" |
||||
Orientation="Vertical"> |
||||
<TextBlock Margin="0,0,4,4" TextAlignment="Center"> |
||||
<Run Text="{x:Static lang:Resources.Label_Page}" /> |
||||
<Run Text="{Binding CurrentPageNumber, FallbackValue=1}" /> |
||||
<Run Text="/" /> |
||||
<Run Text="{Binding TotalPages, FallbackValue=5}" /> |
||||
</TextBlock> |
||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal"> |
||||
<Button |
||||
Margin="0,0,8,0" |
||||
Command="{Binding FirstPage}" |
||||
IsEnabled="{Binding CanGoToFirstPage}" |
||||
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}"> |
||||
<avalonia:Icon Value="fa-solid fa-backward-fast" /> |
||||
</Button> |
||||
<Button |
||||
Margin="0,0,8,0" |
||||
Command="{Binding PreviousPage}" |
||||
IsEnabled="{Binding CanGoToPreviousPage}" |
||||
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}"> |
||||
<avalonia:Icon Value="fa-solid fa-caret-left" /> |
||||
</Button> |
||||
<Button |
||||
Margin="0,0,8,0" |
||||
Command="{Binding NextPage}" |
||||
IsEnabled="{Binding CanGoToNextPage}" |
||||
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}"> |
||||
<avalonia:Icon Value="fa-solid fa-caret-right" /> |
||||
</Button> |
||||
<Button |
||||
Command="{Binding LastPage}" |
||||
IsEnabled="{Binding CanGoToLastPage}" |
||||
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}"> |
||||
<avalonia:Icon Value="fa-solid fa-forward-fast" /> |
||||
</Button> |
||||
</StackPanel> |
||||
</StackPanel> |
||||
|
||||
<TextBlock |
||||
Grid.Row="0" |
||||
Grid.RowSpan="3" |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
FontSize="20" |
||||
IsVisible="{Binding NoResultsFound}" |
||||
Text="{Binding NoResultsText, FallbackValue=No results found}" /> |
||||
|
||||
<controls:ProgressRing |
||||
Grid.Row="1" |
||||
Width="128" |
||||
Height="128" |
||||
HorizontalAlignment="Stretch" |
||||
VerticalAlignment="Stretch" |
||||
IsIndeterminate="True" |
||||
IsVisible="{Binding ShowMainLoadingSpinner, FallbackValue=False}" /> |
||||
</Grid> |
||||
</UserControl> |
||||
<controls:UserControlBase xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" |
||||
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" |
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}" |
||||
d:DesignHeight="700" |
||||
d:DesignWidth="800" |
||||
x:CompileBindings="True" |
||||
x:DataType="viewModels:CheckpointBrowserViewModel" |
||||
mc:Ignorable="d" |
||||
x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage"> |
||||
<TabControl ItemsSource="{Binding Pages}"/> |
||||
</controls:UserControlBase> |
||||
|
@ -0,0 +1,531 @@
|
||||
<controls:UserControlBase |
||||
x:Class="StabilityMatrix.Avalonia.Views.CivitAiBrowserPage" |
||||
xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" |
||||
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" |
||||
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager" |
||||
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" |
||||
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" |
||||
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers" |
||||
d:DataContext="{x:Static designData:DesignData.CivitAiBrowserViewModel}" |
||||
d:DesignHeight="700" |
||||
d:DesignWidth="800" |
||||
x:CompileBindings="True" |
||||
x:DataType="checkpointBrowser:CivitAiBrowserViewModel" |
||||
mc:Ignorable="d"> |
||||
|
||||
<UserControl.Styles> |
||||
<Style Selector="Border#HoverBorder"> |
||||
<Setter Property="Transitions"> |
||||
<Transitions> |
||||
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.237"/> |
||||
</Transitions> |
||||
</Setter> |
||||
|
||||
<Style Selector="^ asyncImageLoader|AdvancedImage"> |
||||
<Setter Property="Transitions"> |
||||
<Transitions> |
||||
<TransformOperationsTransition Property="RenderTransform" |
||||
Duration="0:0:0.237"> |
||||
<TransformOperationsTransition.Easing> |
||||
<QuadraticEaseInOut/> |
||||
</TransformOperationsTransition.Easing> |
||||
</TransformOperationsTransition> |
||||
</Transitions> |
||||
</Setter> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Setter Property="BoxShadow" Value="0 0 40 0 #60000000"/> |
||||
<Setter Property="Cursor" Value="Hand" /> |
||||
<Style Selector="^ asyncImageLoader|AdvancedImage"> |
||||
<Setter Property="CornerRadius" Value="12"/> |
||||
<Setter Property="RenderTransform" Value="scale(1.03, 1.03)"/> |
||||
</Style> |
||||
<Style Selector="^ Border#ModelCardBottom"> |
||||
<Setter Property="Background" Value="#CC000000" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:not(:pointerover)"> |
||||
<Setter Property="BoxShadow" Value="0 0 20 0 #60000000"/> |
||||
<Setter Property="Cursor" Value="Arrow" /> |
||||
<Style Selector="^ asyncImageLoader|AdvancedImage"> |
||||
<Setter Property="CornerRadius" Value="8"/> |
||||
<Setter Property="RenderTransform" Value="scale(1, 1)"/> |
||||
</Style> |
||||
<Style Selector="^ Border#ModelCardBottom"> |
||||
<Setter Property="Background" Value="#99000000" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
</UserControl.Styles> |
||||
|
||||
<UserControl.Resources> |
||||
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/> |
||||
<DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}"> |
||||
<Border |
||||
Name="HoverBorder" |
||||
Padding="0" |
||||
BorderThickness="0" |
||||
Margin="8" |
||||
ClipToBounds="True" |
||||
CornerRadius="8"> |
||||
<Border.ContextFlyout> |
||||
<MenuFlyout> |
||||
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnCivitAi}" |
||||
Command="{Binding OpenModelCommand}"> |
||||
<MenuItem.Icon> |
||||
<ui:SymbolIcon Symbol="Open" /> |
||||
</MenuItem.Icon> |
||||
</MenuItem> |
||||
</MenuFlyout> |
||||
</Border.ContextFlyout> |
||||
<Button |
||||
Name="ModelCard" |
||||
Classes="transparent-full" |
||||
Padding="0" |
||||
BorderThickness="0" |
||||
VerticalContentAlignment="Top" |
||||
CornerRadius="8" |
||||
Command="{Binding ShowVersionDialogCommand}" |
||||
CommandParameter="{Binding CivitModel}" |
||||
IsEnabled="{Binding !IsImporting}"> |
||||
<Grid RowDefinitions="*, Auto"> |
||||
<controls:BetterAdvancedImage |
||||
Grid.RowSpan="2" |
||||
CornerRadius="8" |
||||
Width="330" |
||||
Height="400" |
||||
Source="{Binding CardImage}" |
||||
Stretch="UniformToFill" |
||||
StretchDirection="Both"/> |
||||
|
||||
<StackPanel |
||||
Grid.Row="0" |
||||
HorizontalAlignment="Right" |
||||
Orientation="Horizontal"> |
||||
<Button |
||||
Margin="0,8,8,0" |
||||
HorizontalAlignment="Right" |
||||
VerticalAlignment="Top" |
||||
Classes="transparent-info" |
||||
Command="{Binding ToggleFavoriteCommand}" |
||||
FontSize="20" |
||||
IsVisible="{Binding !IsFavorite}"> |
||||
<Grid> |
||||
<ui:SymbolIcon Symbol="StarAdd" /> |
||||
</Grid> |
||||
</Button> |
||||
<Button |
||||
Margin="0,8,8,0" |
||||
HorizontalAlignment="Right" |
||||
VerticalAlignment="Top" |
||||
Classes="success" |
||||
Command="{Binding ToggleFavoriteCommand}" |
||||
FontSize="20" |
||||
IsVisible="{Binding IsFavorite}"> |
||||
<Grid> |
||||
<ui:SymbolIcon Symbol="StarFilled" /> |
||||
</Grid> |
||||
</Button> |
||||
</StackPanel> |
||||
|
||||
<!-- Username pill card --> |
||||
<Border |
||||
BoxShadow="inset 1.2 0 80 1.8 #66000000" |
||||
CornerRadius="16" |
||||
Margin="4" |
||||
Grid.Row="0" |
||||
HorizontalAlignment="Left" |
||||
VerticalAlignment="Bottom"> |
||||
<Border.Resources> |
||||
<DropShadowEffect |
||||
x:Key="TextDropShadowEffect" |
||||
BlurRadius="12" |
||||
Color="#FF000000" |
||||
Opacity="0.9"/> |
||||
<DropShadowEffect |
||||
x:Key="ImageDropShadowEffect" |
||||
BlurRadius="12" |
||||
Color="#FF000000" |
||||
Opacity="0.2"/> |
||||
</Border.Resources> |
||||
<Button |
||||
Command="{x:Static helpers:IOCommands.OpenUrlCommand}" |
||||
CommandParameter="{Binding CivitModel.Creator.ProfileUrl}" |
||||
CornerRadius="16" |
||||
Classes="transparent" |
||||
Padding="10,4"> |
||||
<StackPanel Orientation="Horizontal" Spacing="6"> |
||||
<controls:BetterAdvancedImage |
||||
Width="22" |
||||
Height="22" |
||||
Effect="{StaticResource ImageDropShadowEffect}" |
||||
CornerRadius="11" |
||||
RenderOptions.BitmapInterpolationMode="HighQuality" |
||||
IsVisible="{Binding CivitModel.Creator.Image, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" |
||||
Source="{Binding CivitModel.Creator.Image}"/> |
||||
<TextBlock |
||||
VerticalAlignment="Center" |
||||
Effect="{StaticResource TextDropShadowEffect}" |
||||
Text="{Binding CivitModel.Creator.Username}"/> |
||||
</StackPanel> |
||||
</Button> |
||||
</Border> |
||||
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal"> |
||||
<controls:Card |
||||
Height="24" |
||||
Margin="8,8,0,0" |
||||
Padding="4" |
||||
HorizontalAlignment="Left" |
||||
VerticalAlignment="Top" |
||||
Classes="info"> |
||||
|
||||
<TextBlock |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
FontSize="11" |
||||
FontWeight="Medium" |
||||
Text="{Binding CivitModel.Type}" /> |
||||
</controls:Card> |
||||
<controls:Card |
||||
Height="24" |
||||
Margin="4,8,0,0" |
||||
Padding="4" |
||||
HorizontalAlignment="Left" |
||||
VerticalAlignment="Top" |
||||
Classes="info"> |
||||
|
||||
<TextBlock |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
FontSize="11" |
||||
FontWeight="Medium" |
||||
Text="{Binding CivitModel.BaseModelType}" /> |
||||
</controls:Card> |
||||
<controls:Card |
||||
Height="24" |
||||
Margin="4,8,0,0" |
||||
Padding="4" |
||||
HorizontalAlignment="Left" |
||||
VerticalAlignment="Top" |
||||
Classes="success" |
||||
IsVisible="{Binding ShowUpdateCard}"> |
||||
|
||||
<TextBlock |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
FontSize="11" |
||||
FontWeight="Medium" |
||||
Text="{Binding UpdateCardText}" /> |
||||
</controls:Card> |
||||
</StackPanel> |
||||
<Border |
||||
Grid.Row="0" |
||||
Grid.RowSpan="2" |
||||
Margin="0,0,0,0" |
||||
HorizontalAlignment="Stretch" |
||||
VerticalAlignment="Stretch" |
||||
Background="#DD000000" |
||||
CornerRadius="8" |
||||
IsVisible="{Binding IsImporting}" |
||||
ZIndex="1" /> |
||||
<StackPanel |
||||
Grid.Row="0" |
||||
Grid.RowSpan="2" |
||||
HorizontalAlignment="Stretch" |
||||
VerticalAlignment="Center" |
||||
IsVisible="{Binding IsImporting}" |
||||
Orientation="Vertical" |
||||
ZIndex="2"> |
||||
<controls:ProgressRing |
||||
Width="120" |
||||
Height="120" |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
EndAngle="450" |
||||
IsIndeterminate="False" |
||||
StartAngle="90" |
||||
Value="{Binding Value}" /> |
||||
<TextBlock |
||||
Margin="0,8,0,0" |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
Text="{Binding Text, TargetNullValue=Importing...}" /> |
||||
</StackPanel> |
||||
|
||||
<Border |
||||
Name="ModelCardBottom" |
||||
Grid.Row="1"> |
||||
<Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto, Auto, Auto"> |
||||
|
||||
<!-- |
||||
TextTrimming causing issues with unicode chars until |
||||
https://github.com/AvaloniaUI/Avalonia/pull/13385 is released |
||||
--> |
||||
<TextBlock |
||||
Grid.ColumnSpan="2" |
||||
MaxWidth="250" |
||||
Margin="8,0,8,0" |
||||
HorizontalAlignment="Left" |
||||
VerticalAlignment="Center" |
||||
FontWeight="SemiBold" |
||||
Foreground="{DynamicResource TextControlForeground}" |
||||
LetterSpacing="0.33" |
||||
Text="{Binding CivitModel.Name}" |
||||
TextWrapping="NoWrap" |
||||
ToolTip.Tip="{Binding CivitModel.Name}" /> |
||||
|
||||
<TextBlock |
||||
Grid.Row="1" |
||||
Grid.Column="0" |
||||
Margin="8,-4,0,0" |
||||
VerticalAlignment="Center" |
||||
FontSize="11" |
||||
Foreground="{DynamicResource TextControlForeground}" |
||||
Text="{Binding CivitModel.LatestModelVersionName, FallbackValue=''}" /> |
||||
|
||||
<StackPanel |
||||
Grid.Row="2" |
||||
Grid.Column="0" |
||||
Orientation="Horizontal"> |
||||
|
||||
<controls:StarsRating |
||||
Margin="8,8,0,8" |
||||
Background="#66000000" |
||||
FontSize="16" |
||||
Foreground="{DynamicResource ThemeEldenRingOrangeColor}" |
||||
Value="{Binding CivitModel.Stats.Rating}" /> |
||||
<TextBlock |
||||
Margin="4,0,0,0" |
||||
VerticalAlignment="Center" |
||||
Text="{Binding CivitModel.Stats.RatingCount}" |
||||
TextAlignment="Center" /> |
||||
</StackPanel> |
||||
|
||||
<StackPanel |
||||
Grid.Row="2" |
||||
Grid.Column="1" |
||||
HorizontalAlignment="Right" |
||||
Orientation="Horizontal"> |
||||
<avalonia:Icon Value="fa-solid fa-heart" /> |
||||
<TextBlock |
||||
Margin="4,0" |
||||
VerticalAlignment="Center" |
||||
Text="{Binding CivitModel.Stats.FavoriteCount, Converter={StaticResource KiloFormatterConverter}}" /> |
||||
|
||||
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" /> |
||||
<TextBlock |
||||
Margin="0,0,4,0" |
||||
VerticalAlignment="Center" |
||||
Text="{Binding CivitModel.Stats.DownloadCount, Converter={StaticResource KiloFormatterConverter}}" /> |
||||
</StackPanel> |
||||
<Button |
||||
Grid.Row="0" |
||||
Grid.Column="1" |
||||
Width="32" |
||||
Margin="0,4,4,0" |
||||
HorizontalAlignment="Right" |
||||
VerticalAlignment="Top" |
||||
HorizontalContentAlignment="Right" |
||||
VerticalContentAlignment="Top" |
||||
BorderThickness="0" |
||||
Classes="transparent"> |
||||
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" /> |
||||
<Button.Flyout> |
||||
<MenuFlyout> |
||||
<MenuItem Command="{Binding OpenModelCommand}" Header="{x:Static lang:Resources.Action_OpenOnCivitAi}"> |
||||
<MenuItem.Icon> |
||||
<ui:SymbolIcon Symbol="Open" /> |
||||
</MenuItem.Icon> |
||||
</MenuItem> |
||||
</MenuFlyout> |
||||
</Button.Flyout> |
||||
</Button> |
||||
</Grid> |
||||
</Border> |
||||
</Grid> |
||||
</Button> |
||||
</Border> |
||||
|
||||
</DataTemplate> |
||||
</UserControl.Resources> |
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto"> |
||||
|
||||
<StackPanel Margin="8" Orientation="Vertical"> |
||||
<Grid ColumnDefinitions="*,Auto"> |
||||
|
||||
<TextBox |
||||
HorizontalAlignment="Stretch" |
||||
KeyDown="InputElement_OnKeyDown" |
||||
Text="{Binding SearchQuery, Mode=TwoWay}" |
||||
Watermark="{x:Static lang:Resources.Label_ModelSearchWatermark}"> |
||||
<TextBox.InnerRightContent> |
||||
<Button |
||||
Classes="transparent-full" |
||||
Command="{Binding ClearSearchQuery}" |
||||
IsVisible="{Binding SearchQuery.Length}"> |
||||
<ui:SymbolIcon Symbol="Cancel" /> |
||||
</Button> |
||||
</TextBox.InnerRightContent> |
||||
</TextBox> |
||||
|
||||
<Button |
||||
Grid.Column="1" |
||||
Width="80" |
||||
Margin="8,0,8,0" |
||||
VerticalAlignment="Stretch" |
||||
Classes="accent" |
||||
Command="{Binding SearchModelsCommand}" |
||||
IsDefault="True"> |
||||
<Grid> |
||||
<controls:ProgressRing |
||||
MinWidth="16" |
||||
MinHeight="16" |
||||
VerticalAlignment="Center" |
||||
BorderThickness="4" |
||||
IsIndeterminate="True" |
||||
IsVisible="{Binding SearchModelsCommand.IsRunning}" /> |
||||
<TextBlock |
||||
VerticalAlignment="Center" |
||||
IsVisible="{Binding !SearchModelsCommand.IsRunning}" |
||||
Text="{x:Static lang:Resources.Action_Search}" /> |
||||
</Grid> |
||||
</Button> |
||||
</Grid> |
||||
<DockPanel> |
||||
<StackPanel Margin="0,8,4,8" Orientation="Vertical"> |
||||
<Label Content="{x:Static lang:Resources.Label_Sort}" /> |
||||
<ComboBox |
||||
MinWidth="100" |
||||
ItemsSource="{Binding AllSortModes}" |
||||
SelectedItem="{Binding SortMode}" /> |
||||
</StackPanel> |
||||
|
||||
<StackPanel Margin="4,8" Orientation="Vertical"> |
||||
<Label Content="{x:Static lang:Resources.Label_TimePeriod}" /> |
||||
<ComboBox |
||||
MinWidth="100" |
||||
ItemsSource="{Binding AllCivitPeriods}" |
||||
SelectedItem="{Binding SelectedPeriod}" /> |
||||
</StackPanel> |
||||
|
||||
<StackPanel Margin="4,8" Orientation="Vertical"> |
||||
<Label Content="{x:Static lang:Resources.Label_ModelType}" /> |
||||
<ComboBox |
||||
MinWidth="100" |
||||
ItemsSource="{Binding AllModelTypes}" |
||||
SelectedItem="{Binding SelectedModelType}" /> |
||||
</StackPanel> |
||||
|
||||
<StackPanel Margin="4,8" Orientation="Vertical"> |
||||
<Label Content="{x:Static lang:Resources.Label_BaseModel}" /> |
||||
<ComboBox |
||||
MinWidth="100" |
||||
ItemsSource="{Binding BaseModelOptions}" |
||||
SelectedItem="{Binding SelectedBaseModelType}" /> |
||||
</StackPanel> |
||||
|
||||
|
||||
<CheckBox |
||||
Margin="8,8,0,0" |
||||
HorizontalAlignment="Right" |
||||
Content="{x:Static lang:Resources.Label_ShowNsfwContent}" |
||||
IsChecked="{Binding ShowNsfw, Mode=TwoWay}" /> |
||||
</DockPanel> |
||||
|
||||
</StackPanel> |
||||
|
||||
<ScrollViewer |
||||
Grid.Row="1" |
||||
ScrollChanged="ScrollViewer_OnScrollChanged"> |
||||
<ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}" |
||||
HorizontalAlignment="Center" |
||||
ItemsSource="{Binding ModelCards}"> |
||||
<ItemsRepeater.Layout> |
||||
<UniformGridLayout Orientation="Horizontal" /> |
||||
</ItemsRepeater.Layout> |
||||
</ItemsRepeater> |
||||
</ScrollViewer> |
||||
|
||||
<TextBlock |
||||
Grid.Row="2" |
||||
Margin="8,8" |
||||
VerticalAlignment="Center" |
||||
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}" /> |
||||
|
||||
<StackPanel Grid.Row="2" |
||||
HorizontalAlignment="Center" |
||||
IsVisible="{Binding HasSearched}" |
||||
Margin="0,8,0,8" |
||||
Orientation="Horizontal"> |
||||
<Button |
||||
Margin="0,0,8,0" |
||||
Command="{Binding FirstPage}" |
||||
IsEnabled="{Binding CanGoToFirstPage}" |
||||
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}"> |
||||
<avalonia:Icon Value="fa-solid fa-backward-fast" /> |
||||
</Button> |
||||
<Button |
||||
Margin="0,0,16,0" |
||||
Command="{Binding PreviousPage}" |
||||
IsEnabled="{Binding CanGoToPreviousPage}" |
||||
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}"> |
||||
<avalonia:Icon Value="fa-solid fa-caret-left" /> |
||||
</Button> |
||||
<TextBlock Margin="8,0,4,0" TextAlignment="Center" |
||||
Text="{x:Static lang:Resources.Label_Page}" |
||||
VerticalAlignment="Center"/> |
||||
<ui:NumberBox Value="{Binding CurrentPageNumber, FallbackValue=1}" |
||||
VerticalAlignment="Center" |
||||
SpinButtonPlacementMode="Hidden" |
||||
TextAlignment="Center"/> |
||||
<TextBlock Margin="4,0,8,0" VerticalAlignment="Center"> |
||||
<Run Text="/"/> |
||||
<Run Text="{Binding TotalPages, FallbackValue=5}"/> |
||||
</TextBlock> |
||||
<Button |
||||
Margin="16,0,8,0" |
||||
Command="{Binding NextPage}" |
||||
IsEnabled="{Binding CanGoToNextPage}" |
||||
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}"> |
||||
<avalonia:Icon Value="fa-solid fa-caret-right" /> |
||||
</Button> |
||||
<Button |
||||
Command="{Binding LastPage}" |
||||
IsEnabled="{Binding CanGoToLastPage}" |
||||
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}"> |
||||
<avalonia:Icon Value="fa-solid fa-forward-fast" /> |
||||
</Button> |
||||
</StackPanel> |
||||
|
||||
<TextBlock |
||||
Grid.Row="0" |
||||
Grid.RowSpan="3" |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
FontSize="20" |
||||
IsVisible="{Binding NoResultsFound}" |
||||
Text="{Binding NoResultsText, FallbackValue=No results found}" /> |
||||
|
||||
<controls:ProgressRing |
||||
Grid.Row="1" |
||||
Width="128" |
||||
Height="128" |
||||
HorizontalAlignment="Stretch" |
||||
VerticalAlignment="Stretch" |
||||
IsIndeterminate="True" |
||||
IsVisible="{Binding ShowMainLoadingSpinner, FallbackValue=False}" /> |
||||
</Grid> |
||||
</controls:UserControlBase> |
@ -0,0 +1,41 @@
|
||||
using System.Diagnostics; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Input; |
||||
using Avalonia.Markup.Xaml; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.ViewModels; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Views; |
||||
|
||||
[Singleton] |
||||
public partial class CivitAiBrowserPage : UserControlBase |
||||
{ |
||||
public CivitAiBrowserPage() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
|
||||
private void InitializeComponent() |
||||
{ |
||||
AvaloniaXamlLoader.Load(this); |
||||
} |
||||
|
||||
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) |
||||
{ |
||||
if (sender is not ScrollViewer scrollViewer) |
||||
return; |
||||
|
||||
var isAtEnd = scrollViewer.Offset == scrollViewer.ScrollBarMaximum; |
||||
Debug.WriteLine($"IsAtEnd: {isAtEnd}"); |
||||
} |
||||
|
||||
private void InputElement_OnKeyDown(object? sender, KeyEventArgs e) |
||||
{ |
||||
if (e.Key == Key.Escape && DataContext is CivitAiBrowserViewModel viewModel) |
||||
{ |
||||
viewModel.ClearSearchQuery(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,33 @@
|
||||
<controls:UserControlBase |
||||
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.PropertyGridDialog" |
||||
xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=System.ObjectModel" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:vmDialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs" |
||||
d:DataContext="{x:Static mocks:DesignData.PropertyGridViewModel}" |
||||
d:DesignHeight="450" |
||||
d:DesignWidth="600" |
||||
x:DataType="vmDialogs:PropertyGridViewModel" |
||||
mc:Ignorable="d"> |
||||
<Grid> |
||||
<ItemsControl x:Name="MainItemsControl" ItemsSource="{Binding SelectedObjectItemsSource, Mode=OneTime}"> |
||||
<ItemsControl.ItemTemplate> |
||||
<DataTemplate DataType="componentModel:INotifyPropertyChanged"> |
||||
<controls:BetterPropertyGrid |
||||
AllowFilter="False" |
||||
AllowQuickFilter="False" |
||||
SelectedObject="{Binding Mode=OneTime}" |
||||
IncludedCategories="{Binding $parent[ItemsControl].((vmDialogs:PropertyGridViewModel)DataContext).IncludeCategories, Mode=OneTime, FallbackValue=''}" |
||||
ExcludedCategories="{Binding $parent[ItemsControl].((vmDialogs:PropertyGridViewModel)DataContext).ExcludeCategories, Mode=OneTime, FallbackValue=''}" |
||||
ShowStyle="{Binding $parent[ItemsControl].((vmDialogs:PropertyGridViewModel)DataContext).ShowStyle, Mode=OneTime, FallbackValue=Alphabetic}" |
||||
ShowTitle="False" /> |
||||
</DataTemplate> |
||||
</ItemsControl.ItemTemplate> |
||||
</ItemsControl> |
||||
|
||||
</Grid> |
||||
</controls:UserControlBase> |
@ -0,0 +1,13 @@
|
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Core.Attributes; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Views.Dialogs; |
||||
|
||||
[Transient] |
||||
public partial class PropertyGridDialog : UserControlBase |
||||
{ |
||||
public PropertyGridDialog() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
} |
@ -0,0 +1,188 @@
|
||||
<controls:UserControlBase |
||||
x:Class="StabilityMatrix.Avalonia.Views.HuggingFacePage" |
||||
xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:ui="using:FluentAvalonia.UI.Controls" |
||||
xmlns:huggingFacePage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.HuggingFacePage" |
||||
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers" |
||||
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser" |
||||
d:DataContext="{x:Static mocks:DesignData.HuggingFacePageViewModel}" |
||||
d:DesignHeight="650" |
||||
d:DesignWidth="800" |
||||
x:CompileBindings="True" |
||||
x:DataType="checkpointBrowser:HuggingFacePageViewModel" |
||||
Focusable="True" |
||||
mc:Ignorable="d"> |
||||
<ScrollViewer> |
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto" |
||||
ColumnDefinitions="Auto, *" |
||||
Margin="8" |
||||
HorizontalAlignment="Stretch"> |
||||
<TextBlock Grid.Column="1" |
||||
VerticalAlignment="Center" |
||||
IsVisible="{Binding !!NumSelected}" |
||||
Margin="8,0,8,0"> |
||||
<Run Text="{Binding NumSelected}" /> |
||||
<Run Text="models selected" /> |
||||
</TextBlock> |
||||
|
||||
<ui:CommandBar |
||||
Grid.Row="0" |
||||
Grid.Column="0" |
||||
Margin="0,-1,0,0" |
||||
VerticalAlignment="Center" |
||||
HorizontalAlignment="Left" |
||||
VerticalContentAlignment="Center" |
||||
DefaultLabelPosition="Right"> |
||||
<ui:CommandBar.PrimaryCommands> |
||||
<ui:CommandBarButton |
||||
IsEnabled="{Binding !!NumSelected}" |
||||
IconSource="Download" |
||||
Foreground="{DynamicResource AccentButtonBackground}" |
||||
Command="{Binding ImportSelectedCommand}" |
||||
Label="{x:Static lang:Resources.Action_Import}" /> |
||||
<ui:CommandBarSeparator /> |
||||
<ui:CommandBarButton |
||||
Command="{Binding SelectAll}" |
||||
IconSource="SelectAll" |
||||
Label="{x:Static lang:Resources.Action_SelectAll}" /> |
||||
<ui:CommandBarButton |
||||
IconSource="ClearSelection" |
||||
Command="{Binding ClearSelection}" |
||||
IsEnabled="{Binding !!NumSelected}" |
||||
Label="{x:Static lang:Resources.Action_ClearSelection}" /> |
||||
<ui:CommandBarSeparator IsVisible="{Binding !!NumSelected}" /> |
||||
</ui:CommandBar.PrimaryCommands> |
||||
</ui:CommandBar> |
||||
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" |
||||
Orientation="Vertical" |
||||
Margin="0,8" |
||||
VerticalAlignment="Bottom"> |
||||
<TextBlock Text="{Binding DownloadPercentText}" |
||||
TextAlignment="Center" |
||||
IsVisible="{Binding !!TotalProgress.Total}" |
||||
Margin="0,4" |
||||
HorizontalAlignment="Stretch" /> |
||||
<ProgressBar Value="{Binding TotalProgress.Current}" |
||||
Maximum="{Binding TotalProgress.Total}" |
||||
IsVisible="{Binding !!TotalProgress.Total}" |
||||
IsIndeterminate="False" /> |
||||
</StackPanel> |
||||
|
||||
<ItemsRepeater Grid.Row="2" |
||||
Grid.Column="0" |
||||
Grid.ColumnSpan="2" |
||||
ItemsSource="{Binding Categories}"> |
||||
<ItemsRepeater.Layout> |
||||
<StackLayout Orientation="Vertical" /> |
||||
</ItemsRepeater.Layout> |
||||
<ItemsRepeater.ItemTemplate> |
||||
<DataTemplate DataType="{x:Type huggingFacePage:CategoryViewModel}"> |
||||
<Expander IsExpanded="True" Margin="0,0,0,8"> |
||||
<Expander.Header> |
||||
<StackPanel Orientation="Horizontal"> |
||||
<CheckBox IsChecked="{Binding IsChecked}" |
||||
Content="{Binding Title}" /> |
||||
</StackPanel> |
||||
</Expander.Header> |
||||
<ItemsRepeater ItemsSource="{Binding Items}" |
||||
Margin="8,0"> |
||||
<ItemsRepeater.Layout> |
||||
<UniformGridLayout MinColumnSpacing="8" |
||||
MinRowSpacing="8" |
||||
MinItemWidth="220" |
||||
ItemsStretch="Uniform" /> |
||||
</ItemsRepeater.Layout> |
||||
<ItemsRepeater.ItemTemplate> |
||||
<DataTemplate DataType="{x:Type huggingFacePage:HuggingfaceItemViewModel}"> |
||||
<Button |
||||
HorizontalAlignment="Stretch" |
||||
HorizontalContentAlignment="Stretch" |
||||
Command="{Binding ToggleSelectedCommand}" |
||||
CornerRadius="8"> |
||||
<Button.ContextFlyout> |
||||
<MenuFlyout> |
||||
<MenuItem Command="{x:Static helpers:IOCommands.OpenUrlCommand}" |
||||
CommandParameter="{Binding RepoUrl}" |
||||
Header="{x:Static lang:Resources.Action_OpenOnHuggingFace}"> |
||||
<MenuItem.Icon> |
||||
<ui:SymbolIcon Symbol="Open" /> |
||||
</MenuItem.Icon> |
||||
</MenuItem> |
||||
<MenuItem Header="{x:Static lang:Resources.Label_License}" |
||||
Command="{x:Static helpers:IOCommands.OpenUrlCommand}" |
||||
CommandParameter="{Binding LicenseUrl}"> |
||||
<MenuItem.Icon> |
||||
<ui:SymbolIcon Symbol="Document" /> |
||||
</MenuItem.Icon> |
||||
</MenuItem> |
||||
</MenuFlyout> |
||||
</Button.ContextFlyout> |
||||
<Grid ColumnDefinitions="Auto, *"> |
||||
<CheckBox |
||||
VerticalAlignment="Center" |
||||
IsChecked="{Binding IsSelected}" |
||||
Content="{Binding Item.ModelName}" /> |
||||
<Button |
||||
Grid.Column="1" |
||||
Width="24" |
||||
Margin="0,4,4,0" |
||||
Padding="4" |
||||
HorizontalAlignment="Right" |
||||
VerticalAlignment="Top" |
||||
HorizontalContentAlignment="Right" |
||||
VerticalContentAlignment="Top" |
||||
BorderThickness="0" |
||||
Classes="transparent"> |
||||
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" /> |
||||
<Button.Flyout> |
||||
<MenuFlyout> |
||||
<MenuItem Command="{x:Static helpers:IOCommands.OpenUrlCommand}" |
||||
CommandParameter="{Binding RepoUrl}" |
||||
Header="{x:Static lang:Resources.Action_OpenOnHuggingFace}"> |
||||
<MenuItem.Icon> |
||||
<ui:SymbolIcon Symbol="Open" /> |
||||
</MenuItem.Icon> |
||||
</MenuItem> |
||||
<MenuItem Header="{x:Static lang:Resources.Label_License}" |
||||
Command="{x:Static helpers:IOCommands.OpenUrlCommand}" |
||||
CommandParameter="{Binding LicenseUrl}"> |
||||
<MenuItem.Icon> |
||||
<ui:SymbolIcon Symbol="List" /> |
||||
</MenuItem.Icon> |
||||
</MenuItem> |
||||
</MenuFlyout> |
||||
</Button.Flyout> |
||||
</Button> |
||||
</Grid> |
||||
</Button> |
||||
</DataTemplate> |
||||
</ItemsRepeater.ItemTemplate> |
||||
</ItemsRepeater> |
||||
</Expander> |
||||
</DataTemplate> |
||||
</ItemsRepeater.ItemTemplate> |
||||
</ItemsRepeater> |
||||
<!-- <Expander Grid.Column="0" --> |
||||
<!-- Grid.ColumnSpan="2" --> |
||||
<!-- Grid.Row="3" --> |
||||
<!-- Header="Manual Model Download"> --> |
||||
<!-- <Grid RowDefinitions="Auto, Auto" ColumnDefinitions="*, Auto"> --> |
||||
<!-- <TextBox Grid.Row="1" Grid.Column="0" --> |
||||
<!-- Watermark="Enter HuggingFace repo URL" --> |
||||
<!-- Margin="0,8" /> --> |
||||
<!-- <Button Grid.Row="1" Grid.Column="1" --> |
||||
<!-- Margin="8,8,0,8" --> |
||||
<!-- Classes="accent" --> |
||||
<!-- Content="Import" /> --> |
||||
<!-- </Grid> --> |
||||
<!-- </Expander> --> |
||||
</Grid> |
||||
</ScrollViewer> |
||||
</controls:UserControlBase> |
@ -0,0 +1,13 @@
|
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Core.Attributes; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Views; |
||||
|
||||
[Singleton] |
||||
public partial class HuggingFacePage : UserControlBase |
||||
{ |
||||
public HuggingFacePage() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
} |
@ -1,172 +0,0 @@
|
||||
using System.Diagnostics; |
||||
using System.Runtime.Versioning; |
||||
using System.Text.RegularExpressions; |
||||
using Microsoft.Win32; |
||||
|
||||
namespace StabilityMatrix.Core.Helper; |
||||
|
||||
public static partial class HardwareHelper |
||||
{ |
||||
private static IReadOnlyList<GpuInfo>? cachedGpuInfos; |
||||
|
||||
private static string RunBashCommand(string command) |
||||
{ |
||||
var processInfo = new ProcessStartInfo("bash", "-c \"" + command + "\"") |
||||
{ |
||||
UseShellExecute = false, |
||||
RedirectStandardOutput = true |
||||
}; |
||||
|
||||
var process = Process.Start(processInfo); |
||||
|
||||
process.WaitForExit(); |
||||
|
||||
var output = process.StandardOutput.ReadToEnd(); |
||||
|
||||
return output; |
||||
} |
||||
|
||||
[SupportedOSPlatform("windows")] |
||||
private static IEnumerable<GpuInfo> IterGpuInfoWindows() |
||||
{ |
||||
const string gpuRegistryKeyPath = |
||||
@"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}"; |
||||
|
||||
using var baseKey = Registry.LocalMachine.OpenSubKey(gpuRegistryKeyPath); |
||||
|
||||
if (baseKey == null) yield break; |
||||
|
||||
foreach (var subKeyName in baseKey.GetSubKeyNames().Where(k => k.StartsWith("0"))) |
||||
{ |
||||
using var subKey = baseKey.OpenSubKey(subKeyName); |
||||
if (subKey != null) |
||||
{ |
||||
yield return new GpuInfo |
||||
{ |
||||
Name = subKey.GetValue("DriverDesc")?.ToString(), |
||||
MemoryBytes = Convert.ToUInt64(subKey.GetValue("HardwareInformation.qwMemorySize")), |
||||
}; |
||||
} |
||||
} |
||||
} |
||||
|
||||
[SupportedOSPlatform("linux")] |
||||
private static IEnumerable<GpuInfo> IterGpuInfoLinux() |
||||
{ |
||||
var output = RunBashCommand("lspci | grep VGA"); |
||||
var gpuLines = output.Split("\n"); |
||||
|
||||
foreach (var line in gpuLines) |
||||
{ |
||||
if (string.IsNullOrWhiteSpace(line)) continue; |
||||
|
||||
var gpuId = line.Split(' ')[0]; // The GPU ID is the first part of the line |
||||
var gpuOutput = RunBashCommand($"lspci -v -s {gpuId}"); |
||||
|
||||
ulong memoryBytes = 0; |
||||
string? name = null; |
||||
|
||||
// Parse output with regex |
||||
var match = Regex.Match(gpuOutput, @"VGA compatible controller: ([^\n]*)"); |
||||
if (match.Success) |
||||
{ |
||||
name = match.Groups[1].Value.Trim(); |
||||
} |
||||
|
||||
match = Regex.Match(gpuOutput, @"prefetchable\) \[size=(\\d+)M\]"); |
||||
if (match.Success) |
||||
{ |
||||
memoryBytes = ulong.Parse(match.Groups[1].Value) * 1024 * 1024; |
||||
} |
||||
|
||||
yield return new GpuInfo { Name = name, MemoryBytes = memoryBytes }; |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Yields GpuInfo for each GPU in the system. |
||||
/// </summary> |
||||
public static IEnumerable<GpuInfo> IterGpuInfo() |
||||
{ |
||||
if (Compat.IsWindows) |
||||
{ |
||||
return IterGpuInfoWindows(); |
||||
} |
||||
else if (Compat.IsLinux) |
||||
{ |
||||
// Since this requires shell commands, fetch cached value if available. |
||||
if (cachedGpuInfos is not null) |
||||
{ |
||||
return cachedGpuInfos; |
||||
} |
||||
|
||||
// No cache, fetch and cache. |
||||
cachedGpuInfos = IterGpuInfoLinux().ToList(); |
||||
return cachedGpuInfos; |
||||
} |
||||
// TODO: Implement for macOS |
||||
return Enumerable.Empty<GpuInfo>(); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Return true if the system has at least one Nvidia GPU. |
||||
/// </summary> |
||||
public static bool HasNvidiaGpu() |
||||
{ |
||||
return IterGpuInfo().Any(gpu => gpu.IsNvidia); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Return true if the system has at least one AMD GPU. |
||||
/// </summary> |
||||
public static bool HasAmdGpu() |
||||
{ |
||||
return IterGpuInfo().Any(gpu => gpu.IsAmd); |
||||
} |
||||
|
||||
// Set ROCm for default if AMD and Linux |
||||
public static bool PreferRocm() => !HasNvidiaGpu() |
||||
&& HasAmdGpu() |
||||
&& Compat.IsLinux; |
||||
|
||||
// Set DirectML for default if AMD and Windows |
||||
public static bool PreferDirectML() => !HasNvidiaGpu() |
||||
&& HasAmdGpu() |
||||
&& Compat.IsWindows; |
||||
} |
||||
|
||||
public enum Level |
||||
{ |
||||
Unknown, |
||||
Low, |
||||
Medium, |
||||
High |
||||
} |
||||
|
||||
public record GpuInfo |
||||
{ |
||||
public string? Name { get; init; } = string.Empty; |
||||
public ulong MemoryBytes { get; init; } |
||||
public Level? MemoryLevel => MemoryBytes switch |
||||
{ |
||||
<= 0 => Level.Unknown, |
||||
< 4 * Size.GiB => Level.Low, |
||||
< 8 * Size.GiB => Level.Medium, |
||||
_ => Level.High |
||||
}; |
||||
|
||||
public bool IsNvidia |
||||
{ |
||||
get |
||||
{ |
||||
var name = Name?.ToLowerInvariant(); |
||||
|
||||
if (string.IsNullOrEmpty(name)) return false; |
||||
|
||||
return name.Contains("nvidia") |
||||
|| name.Contains("tesla"); |
||||
} |
||||
} |
||||
|
||||
public bool IsAmd => Name?.ToLowerInvariant().Contains("amd") ?? false; |
||||
} |
@ -0,0 +1,7 @@
|
||||
namespace StabilityMatrix.Core.Helper.HardwareInfo; |
||||
|
||||
public readonly record struct CpuInfo |
||||
{ |
||||
public string ProcessorCaption { get; init; } |
||||
public string ProcessorName { get; init; } |
||||
} |
@ -0,0 +1,31 @@
|
||||
namespace StabilityMatrix.Core.Helper.HardwareInfo; |
||||
|
||||
public record GpuInfo |
||||
{ |
||||
public int Index { get; init; } |
||||
public string? Name { get; init; } = string.Empty; |
||||
public ulong MemoryBytes { get; init; } |
||||
public MemoryLevel? MemoryLevel => |
||||
MemoryBytes switch |
||||
{ |
||||
<= 0 => HardwareInfo.MemoryLevel.Unknown, |
||||
< 4 * Size.GiB => HardwareInfo.MemoryLevel.Low, |
||||
< 8 * Size.GiB => HardwareInfo.MemoryLevel.Medium, |
||||
_ => HardwareInfo.MemoryLevel.High |
||||
}; |
||||
|
||||
public bool IsNvidia |
||||
{ |
||||
get |
||||
{ |
||||
var name = Name?.ToLowerInvariant(); |
||||
|
||||
if (string.IsNullOrEmpty(name)) |
||||
return false; |
||||
|
||||
return name.Contains("nvidia") || name.Contains("tesla"); |
||||
} |
||||
} |
||||
|
||||
public bool IsAmd => Name?.Contains("amd", StringComparison.OrdinalIgnoreCase) ?? false; |
||||
} |
@ -0,0 +1,234 @@
|
||||
using System.ComponentModel; |
||||
using System.Diagnostics; |
||||
using System.Runtime.InteropServices; |
||||
using System.Runtime.Versioning; |
||||
using System.Text.RegularExpressions; |
||||
using Hardware.Info; |
||||
using Microsoft.Win32; |
||||
|
||||
namespace StabilityMatrix.Core.Helper.HardwareInfo; |
||||
|
||||
public static partial class HardwareHelper |
||||
{ |
||||
private static IReadOnlyList<GpuInfo>? cachedGpuInfos; |
||||
|
||||
private static readonly Lazy<IHardwareInfo> HardwareInfoLazy = new(() => new Hardware.Info.HardwareInfo()); |
||||
|
||||
public static IHardwareInfo HardwareInfo => HardwareInfoLazy.Value; |
||||
|
||||
private static string RunBashCommand(string command) |
||||
{ |
||||
var processInfo = new ProcessStartInfo("bash", "-c \"" + command + "\"") |
||||
{ |
||||
UseShellExecute = false, |
||||
RedirectStandardOutput = true |
||||
}; |
||||
|
||||
var process = Process.Start(processInfo); |
||||
|
||||
process.WaitForExit(); |
||||
|
||||
var output = process.StandardOutput.ReadToEnd(); |
||||
|
||||
return output; |
||||
} |
||||
|
||||
[SupportedOSPlatform("windows")] |
||||
private static IEnumerable<GpuInfo> IterGpuInfoWindows() |
||||
{ |
||||
const string gpuRegistryKeyPath = |
||||
@"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}"; |
||||
|
||||
using var baseKey = Registry.LocalMachine.OpenSubKey(gpuRegistryKeyPath); |
||||
|
||||
if (baseKey == null) |
||||
yield break; |
||||
|
||||
var gpuIndex = 0; |
||||
|
||||
foreach (var subKeyName in baseKey.GetSubKeyNames().Where(k => k.StartsWith("0"))) |
||||
{ |
||||
using var subKey = baseKey.OpenSubKey(subKeyName); |
||||
if (subKey != null) |
||||
{ |
||||
yield return new GpuInfo |
||||
{ |
||||
Index = gpuIndex++, |
||||
Name = subKey.GetValue("DriverDesc")?.ToString(), |
||||
MemoryBytes = Convert.ToUInt64(subKey.GetValue("HardwareInformation.qwMemorySize")), |
||||
}; |
||||
} |
||||
} |
||||
} |
||||
|
||||
[SupportedOSPlatform("linux")] |
||||
private static IEnumerable<GpuInfo> IterGpuInfoLinux() |
||||
{ |
||||
var output = RunBashCommand("lspci | grep VGA"); |
||||
var gpuLines = output.Split("\n"); |
||||
|
||||
var gpuIndex = 0; |
||||
|
||||
foreach (var line in gpuLines) |
||||
{ |
||||
if (string.IsNullOrWhiteSpace(line)) |
||||
continue; |
||||
|
||||
var gpuId = line.Split(' ')[0]; // The GPU ID is the first part of the line |
||||
var gpuOutput = RunBashCommand($"lspci -v -s {gpuId}"); |
||||
|
||||
ulong memoryBytes = 0; |
||||
string? name = null; |
||||
|
||||
// Parse output with regex |
||||
var match = Regex.Match(gpuOutput, @"VGA compatible controller: ([^\n]*)"); |
||||
if (match.Success) |
||||
{ |
||||
name = match.Groups[1].Value.Trim(); |
||||
} |
||||
|
||||
match = Regex.Match(gpuOutput, @"prefetchable\) \[size=(\\d+)M\]"); |
||||
if (match.Success) |
||||
{ |
||||
memoryBytes = ulong.Parse(match.Groups[1].Value) * 1024 * 1024; |
||||
} |
||||
|
||||
yield return new GpuInfo |
||||
{ |
||||
Index = gpuIndex++, |
||||
Name = name, |
||||
MemoryBytes = memoryBytes |
||||
}; |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Yields GpuInfo for each GPU in the system. |
||||
/// </summary> |
||||
public static IEnumerable<GpuInfo> IterGpuInfo() |
||||
{ |
||||
if (Compat.IsWindows) |
||||
{ |
||||
return IterGpuInfoWindows(); |
||||
} |
||||
else if (Compat.IsLinux) |
||||
{ |
||||
// Since this requires shell commands, fetch cached value if available. |
||||
if (cachedGpuInfos is not null) |
||||
{ |
||||
return cachedGpuInfos; |
||||
} |
||||
|
||||
// No cache, fetch and cache. |
||||
cachedGpuInfos = IterGpuInfoLinux().ToList(); |
||||
return cachedGpuInfos; |
||||
} |
||||
// TODO: Implement for macOS |
||||
return Enumerable.Empty<GpuInfo>(); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Return true if the system has at least one Nvidia GPU. |
||||
/// </summary> |
||||
public static bool HasNvidiaGpu() |
||||
{ |
||||
return IterGpuInfo().Any(gpu => gpu.IsNvidia); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Return true if the system has at least one AMD GPU. |
||||
/// </summary> |
||||
public static bool HasAmdGpu() |
||||
{ |
||||
return IterGpuInfo().Any(gpu => gpu.IsAmd); |
||||
} |
||||
|
||||
// Set ROCm for default if AMD and Linux |
||||
public static bool PreferRocm() => !HasNvidiaGpu() && HasAmdGpu() && Compat.IsLinux; |
||||
|
||||
// Set DirectML for default if AMD and Windows |
||||
public static bool PreferDirectML() => !HasNvidiaGpu() && HasAmdGpu() && Compat.IsWindows; |
||||
|
||||
/// <summary> |
||||
/// Gets the total and available physical memory in bytes. |
||||
/// </summary> |
||||
public static MemoryInfo GetMemoryInfo() => |
||||
Compat.IsWindows ? GetMemoryInfoImplWindows() : GetMemoryInfoImplGeneric(); |
||||
|
||||
[SupportedOSPlatform("windows")] |
||||
private static MemoryInfo GetMemoryInfoImplWindows() |
||||
{ |
||||
var memoryStatus = new Win32MemoryStatusEx(); |
||||
|
||||
if (!GlobalMemoryStatusEx(ref memoryStatus)) |
||||
{ |
||||
throw new Win32Exception(Marshal.GetLastWin32Error()); |
||||
} |
||||
|
||||
if (!GetPhysicallyInstalledSystemMemory(out var installedMemoryKb)) |
||||
{ |
||||
throw new Win32Exception(Marshal.GetLastWin32Error()); |
||||
} |
||||
|
||||
return new MemoryInfo |
||||
{ |
||||
TotalInstalledBytes = (ulong)installedMemoryKb * 1024, |
||||
TotalPhysicalBytes = memoryStatus.UllTotalPhys, |
||||
AvailablePhysicalBytes = memoryStatus.UllAvailPhys |
||||
}; |
||||
} |
||||
|
||||
private static MemoryInfo GetMemoryInfoImplGeneric() |
||||
{ |
||||
HardwareInfo.RefreshMemoryList(); |
||||
|
||||
return new MemoryInfo |
||||
{ |
||||
TotalPhysicalBytes = HardwareInfo.MemoryStatus.TotalPhysical, |
||||
AvailablePhysicalBytes = HardwareInfo.MemoryStatus.AvailablePhysical |
||||
}; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets cpu info |
||||
/// </summary> |
||||
public static Task<CpuInfo> GetCpuInfoAsync() => |
||||
Compat.IsWindows ? Task.FromResult(GetCpuInfoImplWindows()) : GetCpuInfoImplGenericAsync(); |
||||
|
||||
[SupportedOSPlatform("windows")] |
||||
private static CpuInfo GetCpuInfoImplWindows() |
||||
{ |
||||
var info = new CpuInfo(); |
||||
|
||||
using var processorKey = Registry |
||||
.LocalMachine |
||||
.OpenSubKey(@"Hardware\Description\System\CentralProcessor\0", RegistryKeyPermissionCheck.ReadSubTree); |
||||
|
||||
if (processorKey?.GetValue("ProcessorNameString") is string processorName) |
||||
{ |
||||
info = info with { ProcessorCaption = processorName.Trim() }; |
||||
} |
||||
|
||||
return info; |
||||
} |
||||
|
||||
private static Task<CpuInfo> GetCpuInfoImplGenericAsync() |
||||
{ |
||||
return Task.Run(() => |
||||
{ |
||||
HardwareInfo.RefreshCPUList(); |
||||
|
||||
return new CpuInfo { ProcessorCaption = HardwareInfo.CpuList.FirstOrDefault()?.Caption.Trim() ?? "" }; |
||||
}); |
||||
} |
||||
|
||||
[SupportedOSPlatform("windows")] |
||||
[LibraryImport("kernel32.dll", SetLastError = true)] |
||||
[return: MarshalAs(UnmanagedType.Bool)] |
||||
private static partial bool GetPhysicallyInstalledSystemMemory(out long totalMemoryInKilobytes); |
||||
|
||||
[SupportedOSPlatform("windows")] |
||||
[LibraryImport("kernel32.dll", SetLastError = true)] |
||||
[return: MarshalAs(UnmanagedType.Bool)] |
||||
private static partial bool GlobalMemoryStatusEx(ref Win32MemoryStatusEx lpBuffer); |
||||
} |
@ -0,0 +1,10 @@
|
||||
namespace StabilityMatrix.Core.Helper.HardwareInfo; |
||||
|
||||
public readonly record struct MemoryInfo |
||||
{ |
||||
public ulong TotalInstalledBytes { get; init; } |
||||
|
||||
public ulong TotalPhysicalBytes { get; init; } |
||||
|
||||
public ulong AvailablePhysicalBytes { get; init; } |
||||
} |
@ -0,0 +1,9 @@
|
||||
namespace StabilityMatrix.Core.Helper.HardwareInfo; |
||||
|
||||
public enum MemoryLevel |
||||
{ |
||||
Unknown, |
||||
Low, |
||||
Medium, |
||||
High |
||||
} |
@ -0,0 +1,19 @@
|
||||
using System.Runtime.InteropServices; |
||||
|
||||
namespace StabilityMatrix.Core.Helper.HardwareInfo; |
||||
|
||||
[StructLayout(LayoutKind.Sequential)] |
||||
public struct Win32MemoryStatusEx |
||||
{ |
||||
public uint DwLength = (uint)Marshal.SizeOf(typeof(Win32MemoryStatusEx)); |
||||
public uint DwMemoryLoad = 0; |
||||
public ulong UllTotalPhys = 0; |
||||
public ulong UllAvailPhys = 0; |
||||
public ulong UllTotalPageFile = 0; |
||||
public ulong UllAvailPageFile = 0; |
||||
public ulong UllTotalVirtual = 0; |
||||
public ulong UllAvailVirtual = 0; |
||||
public ulong UllAvailExtendedVirtual = 0; |
||||
|
||||
public Win32MemoryStatusEx() { } |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue