JT
8 months ago
committed by
GitHub
135 changed files with 14890 additions and 1242 deletions
@ -0,0 +1,208 @@
|
||||
using System; |
||||
using System.Threading.Tasks; |
||||
using Avalonia; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Input; |
||||
using Avalonia.Interactivity; |
||||
using Avalonia.Xaml.Interactions.DragAndDrop; |
||||
using Avalonia.Xaml.Interactivity; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls; |
||||
|
||||
public class BetterContextDragBehavior : Behavior<Control> |
||||
{ |
||||
private Point _dragStartPoint; |
||||
private PointerEventArgs? _triggerEvent; |
||||
private bool _lock; |
||||
private bool _captured; |
||||
|
||||
public static readonly StyledProperty<object?> ContextProperty = AvaloniaProperty.Register< |
||||
ContextDragBehavior, |
||||
object? |
||||
>(nameof(Context)); |
||||
|
||||
public static readonly StyledProperty<IDragHandler?> HandlerProperty = AvaloniaProperty.Register< |
||||
ContextDragBehavior, |
||||
IDragHandler? |
||||
>(nameof(Handler)); |
||||
|
||||
public static readonly StyledProperty<double> HorizontalDragThresholdProperty = AvaloniaProperty.Register< |
||||
ContextDragBehavior, |
||||
double |
||||
>(nameof(HorizontalDragThreshold), 3); |
||||
|
||||
public static readonly StyledProperty<double> VerticalDragThresholdProperty = AvaloniaProperty.Register< |
||||
ContextDragBehavior, |
||||
double |
||||
>(nameof(VerticalDragThreshold), 3); |
||||
|
||||
public static readonly StyledProperty<string> DataFormatProperty = AvaloniaProperty.Register< |
||||
BetterContextDragBehavior, |
||||
string |
||||
>("DataFormat"); |
||||
|
||||
public string DataFormat |
||||
{ |
||||
get => GetValue(DataFormatProperty); |
||||
set => SetValue(DataFormatProperty, value); |
||||
} |
||||
|
||||
public object? Context |
||||
{ |
||||
get => GetValue(ContextProperty); |
||||
set => SetValue(ContextProperty, value); |
||||
} |
||||
|
||||
public IDragHandler? Handler |
||||
{ |
||||
get => GetValue(HandlerProperty); |
||||
set => SetValue(HandlerProperty, value); |
||||
} |
||||
|
||||
public double HorizontalDragThreshold |
||||
{ |
||||
get => GetValue(HorizontalDragThresholdProperty); |
||||
set => SetValue(HorizontalDragThresholdProperty, value); |
||||
} |
||||
|
||||
public double VerticalDragThreshold |
||||
{ |
||||
get => GetValue(VerticalDragThresholdProperty); |
||||
set => SetValue(VerticalDragThresholdProperty, value); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
protected override void OnAttachedToVisualTree() |
||||
{ |
||||
AssociatedObject?.AddHandler( |
||||
InputElement.PointerPressedEvent, |
||||
AssociatedObject_PointerPressed, |
||||
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble |
||||
); |
||||
AssociatedObject?.AddHandler( |
||||
InputElement.PointerReleasedEvent, |
||||
AssociatedObject_PointerReleased, |
||||
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble |
||||
); |
||||
AssociatedObject?.AddHandler( |
||||
InputElement.PointerMovedEvent, |
||||
AssociatedObject_PointerMoved, |
||||
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble |
||||
); |
||||
AssociatedObject?.AddHandler( |
||||
InputElement.PointerCaptureLostEvent, |
||||
AssociatedObject_CaptureLost, |
||||
RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble |
||||
); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
protected override void OnDetachedFromVisualTree() |
||||
{ |
||||
AssociatedObject?.RemoveHandler(InputElement.PointerPressedEvent, AssociatedObject_PointerPressed); |
||||
AssociatedObject?.RemoveHandler(InputElement.PointerReleasedEvent, AssociatedObject_PointerReleased); |
||||
AssociatedObject?.RemoveHandler(InputElement.PointerMovedEvent, AssociatedObject_PointerMoved); |
||||
AssociatedObject?.RemoveHandler(InputElement.PointerCaptureLostEvent, AssociatedObject_CaptureLost); |
||||
} |
||||
|
||||
private async Task DoDragDrop(PointerEventArgs triggerEvent, object? value) |
||||
{ |
||||
var data = new DataObject(); |
||||
data.Set(DataFormat, value!); |
||||
|
||||
var effect = DragDropEffects.None; |
||||
|
||||
if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Alt)) |
||||
{ |
||||
effect |= DragDropEffects.Link; |
||||
} |
||||
else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Shift)) |
||||
{ |
||||
effect |= DragDropEffects.Move; |
||||
} |
||||
else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Control)) |
||||
{ |
||||
effect |= DragDropEffects.Copy; |
||||
} |
||||
else |
||||
{ |
||||
effect |= DragDropEffects.Move; |
||||
} |
||||
|
||||
await DragDrop.DoDragDrop(triggerEvent, data, effect); |
||||
} |
||||
|
||||
private void Released() |
||||
{ |
||||
_triggerEvent = null; |
||||
_lock = false; |
||||
} |
||||
|
||||
private void AssociatedObject_PointerPressed(object? sender, PointerPressedEventArgs e) |
||||
{ |
||||
var properties = e.GetCurrentPoint(AssociatedObject).Properties; |
||||
if (properties.IsLeftButtonPressed) |
||||
{ |
||||
if (e.Source is Control control && AssociatedObject?.DataContext == control.DataContext) |
||||
{ |
||||
_dragStartPoint = e.GetPosition(null); |
||||
_triggerEvent = e; |
||||
_lock = true; |
||||
_captured = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void AssociatedObject_PointerReleased(object? sender, PointerReleasedEventArgs e) |
||||
{ |
||||
if (_captured) |
||||
{ |
||||
if (e.InitialPressMouseButton == MouseButton.Left && _triggerEvent is { }) |
||||
{ |
||||
Released(); |
||||
} |
||||
|
||||
_captured = false; |
||||
} |
||||
} |
||||
|
||||
private async void AssociatedObject_PointerMoved(object? sender, PointerEventArgs e) |
||||
{ |
||||
var properties = e.GetCurrentPoint(AssociatedObject).Properties; |
||||
if (_captured && properties.IsLeftButtonPressed && _triggerEvent is { }) |
||||
{ |
||||
var point = e.GetPosition(null); |
||||
var diff = _dragStartPoint - point; |
||||
var horizontalDragThreshold = HorizontalDragThreshold; |
||||
var verticalDragThreshold = VerticalDragThreshold; |
||||
|
||||
if (Math.Abs(diff.X) > horizontalDragThreshold || Math.Abs(diff.Y) > verticalDragThreshold) |
||||
{ |
||||
if (_lock) |
||||
{ |
||||
_lock = false; |
||||
} |
||||
else |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
var context = Context ?? AssociatedObject?.DataContext; |
||||
|
||||
Handler?.BeforeDragDrop(sender, _triggerEvent, context); |
||||
|
||||
await DoDragDrop(_triggerEvent, context); |
||||
|
||||
Handler?.AfterDragDrop(sender, _triggerEvent, context); |
||||
|
||||
_triggerEvent = null; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void AssociatedObject_CaptureLost(object? sender, PointerCaptureLostEventArgs e) |
||||
{ |
||||
Released(); |
||||
_captured = false; |
||||
} |
||||
} |
@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Models; |
||||
|
||||
public class OpenArtCustomNode |
||||
{ |
||||
public required string Title { get; set; } |
||||
public List<string> Children { get; set; } = []; |
||||
public bool IsInstalled { get; set; } |
||||
} |
@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Text.Json.Serialization; |
||||
using Avalonia.Platform.Storage; |
||||
using StabilityMatrix.Core.Models.Api.OpenArt; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Models; |
||||
|
||||
public class OpenArtMetadata |
||||
{ |
||||
[JsonPropertyName("sm_workflow_data")] |
||||
public OpenArtSearchResult? Workflow { get; set; } |
||||
|
||||
[JsonIgnore] |
||||
public string? FirstThumbnail => Workflow?.Thumbnails?.Select(x => x.Url).FirstOrDefault()?.ToString(); |
||||
|
||||
[JsonIgnore] |
||||
public List<IStorageFile>? FilePath { get; set; } |
||||
|
||||
[JsonIgnore] |
||||
public bool HasMetadata => Workflow?.Creator != null; |
||||
} |
@ -1,7 +1,10 @@
|
||||
namespace StabilityMatrix.Avalonia.Models; |
||||
using System.Collections.ObjectModel; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Models; |
||||
|
||||
public class PackageOutputCategory |
||||
{ |
||||
public ObservableCollection<PackageOutputCategory> SubDirectories { get; set; } = new(); |
||||
public required string Name { get; set; } |
||||
public required string Path { get; set; } |
||||
} |
||||
|
@ -0,0 +1,8 @@
|
||||
using System.Threading.Tasks; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Services; |
||||
|
||||
public interface IModelDownloadLinkHandler |
||||
{ |
||||
Task StartListening(); |
||||
} |
@ -0,0 +1,248 @@
|
||||
using System; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using System.Web; |
||||
using Avalonia.Controls.Notifications; |
||||
using Avalonia.Threading; |
||||
using MessagePipe; |
||||
using Microsoft.Extensions.Logging; |
||||
using StabilityMatrix.Avalonia.Helpers; |
||||
using StabilityMatrix.Core.Api; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Extensions; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Api; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Services; |
||||
|
||||
[Singleton(typeof(IModelDownloadLinkHandler)), Singleton(typeof(IAsyncDisposable))] |
||||
public class ModelDownloadLinkHandler( |
||||
IDistributedSubscriber<string, Uri> uriHandlerSubscriber, |
||||
ILogger<ModelDownloadLinkHandler> logger, |
||||
ICivitApi civitApi, |
||||
INotificationService notificationService, |
||||
ISettingsManager settingsManager, |
||||
IDownloadService downloadService, |
||||
ITrackedDownloadService trackedDownloadService |
||||
) : IAsyncDisposable, IModelDownloadLinkHandler |
||||
{ |
||||
private IAsyncDisposable? uriHandlerSubscription; |
||||
private const string DownloadCivitModel = "downloadCivitModel"; |
||||
|
||||
public async Task StartListening() |
||||
{ |
||||
uriHandlerSubscription = await uriHandlerSubscriber.SubscribeAsync( |
||||
UriHandler.IpcKeySend, |
||||
UriReceivedHandler |
||||
); |
||||
} |
||||
|
||||
public async ValueTask DisposeAsync() |
||||
{ |
||||
if (uriHandlerSubscription is not null) |
||||
{ |
||||
await uriHandlerSubscription.DisposeAsync(); |
||||
uriHandlerSubscription = null; |
||||
} |
||||
} |
||||
|
||||
private void UriReceivedHandler(Uri receivedUri) |
||||
{ |
||||
logger.LogDebug("ModelDownloadLinkHandler Received URI: {Uri}", receivedUri.PathAndQuery); |
||||
if (!receivedUri.Host.Equals(DownloadCivitModel, StringComparison.OrdinalIgnoreCase)) |
||||
return; |
||||
|
||||
var queryDict = HttpUtility.ParseQueryString(receivedUri.Query); |
||||
var modelIdStr = queryDict["modelId"]; |
||||
var modelVersionIdStr = queryDict["modelVersionId"]; |
||||
var type = queryDict["type"]; |
||||
var format = queryDict["format"]; |
||||
var size = queryDict["size"]; |
||||
var fp = queryDict["fp"]; |
||||
|
||||
if ( |
||||
string.IsNullOrWhiteSpace(modelIdStr) |
||||
|| string.IsNullOrWhiteSpace(type) |
||||
|| string.IsNullOrWhiteSpace(format) |
||||
|| !int.TryParse(modelIdStr, out var modelId) |
||||
|| !Enum.TryParse<CivitFileType>(type, out var civitFileType) |
||||
|| !Enum.TryParse<CivitModelFormat>(format, out var civitFormat) |
||||
) |
||||
{ |
||||
logger.LogError("ModelDownloadLinkHandler: Invalid query parameters"); |
||||
|
||||
Dispatcher.UIThread.Post( |
||||
() => |
||||
notificationService.Show( |
||||
new Notification( |
||||
"Invalid Download Link", |
||||
"The download link is invalid", |
||||
NotificationType.Error |
||||
) |
||||
) |
||||
); |
||||
return; |
||||
} |
||||
|
||||
Dispatcher.UIThread.Post( |
||||
() => |
||||
notificationService.Show( |
||||
"Link Received", |
||||
"Successfully received download link", |
||||
NotificationType.Warning |
||||
) |
||||
); |
||||
|
||||
var modelTask = civitApi.GetModelById(modelId); |
||||
modelTask.Wait(); |
||||
var model = modelTask.Result; |
||||
|
||||
var useModelVersion = !string.IsNullOrWhiteSpace(modelVersionIdStr); |
||||
var modelVersionId = useModelVersion ? int.Parse(modelVersionIdStr) : 0; |
||||
|
||||
var modelVersion = useModelVersion |
||||
? model.ModelVersions?.FirstOrDefault(x => x.Id == modelVersionId) |
||||
: model.ModelVersions?.FirstOrDefault(); |
||||
|
||||
if (modelVersion is null) |
||||
{ |
||||
logger.LogError("ModelDownloadLinkHandler: Model version not found"); |
||||
Dispatcher.UIThread.Post( |
||||
() => |
||||
notificationService.Show( |
||||
new Notification( |
||||
"Model has no versions available", |
||||
"This model has no versions available for download", |
||||
NotificationType.Error |
||||
) |
||||
) |
||||
); |
||||
return; |
||||
} |
||||
|
||||
var possibleFiles = modelVersion.Files?.Where( |
||||
x => x.Type == civitFileType && x.Metadata.Format == civitFormat |
||||
); |
||||
|
||||
if (!string.IsNullOrWhiteSpace(fp) && Enum.TryParse<CivitModelFpType>(fp, out var fpType)) |
||||
{ |
||||
possibleFiles = possibleFiles?.Where(x => x.Metadata.Fp == fpType); |
||||
} |
||||
|
||||
if (!string.IsNullOrWhiteSpace(size) && Enum.TryParse<CivitModelSize>(size, out var modelSize)) |
||||
{ |
||||
possibleFiles = possibleFiles?.Where(x => x.Metadata.Size == modelSize); |
||||
} |
||||
|
||||
possibleFiles = possibleFiles?.ToList(); |
||||
|
||||
if (possibleFiles is null) |
||||
{ |
||||
Dispatcher.UIThread.Post( |
||||
() => |
||||
notificationService.Show( |
||||
new Notification( |
||||
"Model has no files available", |
||||
"This model has no files available for download", |
||||
NotificationType.Error |
||||
) |
||||
) |
||||
); |
||||
logger.LogError("ModelDownloadLinkHandler: Model file not found"); |
||||
return; |
||||
} |
||||
|
||||
var selectedFile = possibleFiles.FirstOrDefault() ?? modelVersion.Files?.FirstOrDefault(); |
||||
|
||||
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); |
||||
var downloadDirectory = rootModelsDirectory.JoinDir( |
||||
selectedFile.Type == CivitFileType.VAE |
||||
? SharedFolderType.VAE.GetStringValue() |
||||
: model.Type.ConvertTo<SharedFolderType>().GetStringValue() |
||||
); |
||||
|
||||
downloadDirectory.Create(); |
||||
var downloadPath = downloadDirectory.JoinFile(selectedFile.Name); |
||||
|
||||
// Create tracked download |
||||
var download = trackedDownloadService.NewDownload(selectedFile.DownloadUrl, downloadPath); |
||||
|
||||
// Download model info and preview first |
||||
var saveCmInfoTask = SaveCmInfo(model, modelVersion, selectedFile, downloadDirectory); |
||||
var savePreviewImageTask = SavePreviewImage(modelVersion, downloadPath); |
||||
|
||||
Task.WaitAll([saveCmInfoTask, savePreviewImageTask]); |
||||
|
||||
var cmInfoPath = saveCmInfoTask.Result; |
||||
var previewImagePath = savePreviewImageTask.Result; |
||||
|
||||
// Add hash info |
||||
download.ExpectedHashSha256 = selectedFile.Hashes.SHA256; |
||||
|
||||
// Add files to cleanup list |
||||
download.ExtraCleanupFileNames.Add(cmInfoPath); |
||||
if (previewImagePath is not null) |
||||
{ |
||||
download.ExtraCleanupFileNames.Add(previewImagePath); |
||||
} |
||||
|
||||
// Add hash context action |
||||
download.ContextAction = CivitPostDownloadContextAction.FromCivitFile(selectedFile); |
||||
|
||||
download.Start(); |
||||
|
||||
Dispatcher.UIThread.Post( |
||||
() => notificationService.Show("Download Started", $"Downloading {selectedFile.Name}") |
||||
); |
||||
} |
||||
|
||||
private static async Task<FilePath> SaveCmInfo( |
||||
CivitModel model, |
||||
CivitModelVersion modelVersion, |
||||
CivitFile modelFile, |
||||
DirectoryPath downloadDirectory |
||||
) |
||||
{ |
||||
var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Name); |
||||
var modelInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTime.UtcNow); |
||||
|
||||
await modelInfo.SaveJsonToDirectory(downloadDirectory, modelFileName); |
||||
|
||||
var jsonName = $"{modelFileName}.cm-info.json"; |
||||
return downloadDirectory.JoinFile(jsonName); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Saves the preview image to the same directory as the model file |
||||
/// </summary> |
||||
/// <param name="modelVersion"></param> |
||||
/// <param name="modelFilePath"></param> |
||||
/// <returns>The file path of the saved preview image</returns> |
||||
private async Task<FilePath?> SavePreviewImage(CivitModelVersion modelVersion, FilePath modelFilePath) |
||||
{ |
||||
// Skip if model has no images |
||||
if (modelVersion.Images == null || modelVersion.Images.Count == 0) |
||||
{ |
||||
return null; |
||||
} |
||||
|
||||
var image = modelVersion.Images[0]; |
||||
var imageExtension = Path.GetExtension(image.Url).TrimStart('.'); |
||||
if (imageExtension is "jpg" or "jpeg" or "png") |
||||
{ |
||||
var imageDownloadPath = modelFilePath.Directory!.JoinFile( |
||||
$"{modelFilePath.NameWithoutExtension}.preview.{imageExtension}" |
||||
); |
||||
|
||||
var imageTask = downloadService.DownloadToFileAsync(image.Url, imageDownloadPath); |
||||
await notificationService.TryAsync(imageTask, "Could not download preview image"); |
||||
|
||||
return imageDownloadPath; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
} |
@ -0,0 +1,148 @@
|
||||
using System; |
||||
using System.Collections.Immutable; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using Avalonia.Controls.Notifications; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using Microsoft.Extensions.Logging; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
using StabilityMatrix.Avalonia.ViewModels; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Extensions; |
||||
using StabilityMatrix.Core.Helper.Factory; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.FileInterfaces; |
||||
using StabilityMatrix.Core.Models.Packages; |
||||
using StabilityMatrix.Core.Python; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Services; |
||||
|
||||
[Singleton] |
||||
public partial class RunningPackageService( |
||||
ILogger<RunningPackageService> logger, |
||||
IPackageFactory packageFactory, |
||||
INotificationService notificationService, |
||||
ISettingsManager settingsManager, |
||||
IPyRunner pyRunner |
||||
) : ObservableObject |
||||
{ |
||||
// 🤔 what if we put the ConsoleViewModel inside the BasePackage? 🤔 |
||||
[ObservableProperty] |
||||
private ObservableDictionary<Guid, RunningPackageViewModel> runningPackages = []; |
||||
|
||||
public async Task<PackagePair?> StartPackage(InstalledPackage installedPackage, string? command = null) |
||||
{ |
||||
var activeInstallName = installedPackage.PackageName; |
||||
var basePackage = string.IsNullOrWhiteSpace(activeInstallName) |
||||
? null |
||||
: packageFactory.GetNewBasePackage(installedPackage); |
||||
|
||||
if (basePackage == null) |
||||
{ |
||||
logger.LogWarning( |
||||
"During launch, package name '{PackageName}' did not match a definition", |
||||
activeInstallName |
||||
); |
||||
|
||||
notificationService.Show( |
||||
new Notification( |
||||
"Package name invalid", |
||||
"Install package name did not match a definition. Please reinstall and let us know about this issue.", |
||||
NotificationType.Error |
||||
) |
||||
); |
||||
return null; |
||||
} |
||||
|
||||
// If this is the first launch (LaunchArgs is null), |
||||
// load and save a launch options dialog vm |
||||
// so that dynamic initial values are saved. |
||||
if (installedPackage.LaunchArgs == null) |
||||
{ |
||||
var definitions = basePackage.LaunchOptions; |
||||
// Create config cards and save them |
||||
var cards = LaunchOptionCard |
||||
.FromDefinitions(definitions, Array.Empty<LaunchOption>()) |
||||
.ToImmutableArray(); |
||||
|
||||
var args = cards.SelectMany(c => c.Options).ToList(); |
||||
|
||||
logger.LogDebug( |
||||
"Setting initial launch args: {Args}", |
||||
string.Join(", ", args.Select(o => o.ToArgString()?.ToRepr())) |
||||
); |
||||
|
||||
settingsManager.SaveLaunchArgs(installedPackage.Id, args); |
||||
} |
||||
|
||||
if (basePackage is not StableSwarm) |
||||
{ |
||||
await pyRunner.Initialize(); |
||||
} |
||||
|
||||
// Get path from package |
||||
var packagePath = new DirectoryPath(settingsManager.LibraryDir, installedPackage.LibraryPath!); |
||||
|
||||
if (basePackage is not StableSwarm) |
||||
{ |
||||
// Unpack sitecustomize.py to venv |
||||
await UnpackSiteCustomize(packagePath.JoinDir("venv")); |
||||
} |
||||
|
||||
// Clear console and start update processing |
||||
var console = new ConsoleViewModel(); |
||||
console.StartUpdates(); |
||||
|
||||
// Update shared folder links (in case library paths changed) |
||||
await basePackage.UpdateModelFolders( |
||||
packagePath, |
||||
installedPackage.PreferredSharedFolderMethod ?? basePackage.RecommendedSharedFolderMethod |
||||
); |
||||
|
||||
// Load user launch args from settings and convert to string |
||||
var userArgs = installedPackage.LaunchArgs ?? []; |
||||
var userArgsString = string.Join(" ", userArgs.Select(opt => opt.ToArgString())); |
||||
|
||||
// Join with extras, if any |
||||
userArgsString = string.Join(" ", userArgsString, basePackage.ExtraLaunchArguments); |
||||
|
||||
// Use input command if provided, otherwise use package launch command |
||||
command ??= basePackage.LaunchCommand; |
||||
|
||||
await basePackage.RunPackage(packagePath, command, userArgsString, o => console.Post(o)); |
||||
var runningPackage = new PackagePair(installedPackage, basePackage); |
||||
|
||||
var viewModel = new RunningPackageViewModel( |
||||
settingsManager, |
||||
notificationService, |
||||
this, |
||||
runningPackage, |
||||
console |
||||
); |
||||
RunningPackages.Add(runningPackage.InstalledPackage.Id, viewModel); |
||||
|
||||
return runningPackage; |
||||
} |
||||
|
||||
public async Task StopPackage(Guid id) |
||||
{ |
||||
if (RunningPackages.TryGetValue(id, out var vm)) |
||||
{ |
||||
var runningPackage = vm.RunningPackage; |
||||
await runningPackage.BasePackage.WaitForShutdown(); |
||||
RunningPackages.Remove(id); |
||||
} |
||||
} |
||||
|
||||
public RunningPackageViewModel? GetRunningPackageViewModel(Guid id) => |
||||
RunningPackages.TryGetValue(id, out var vm) ? vm : null; |
||||
|
||||
private static async Task UnpackSiteCustomize(DirectoryPath venvPath) |
||||
{ |
||||
var sitePackages = venvPath.JoinDir(PyVenvRunner.RelativeSitePackagesPath); |
||||
var file = sitePackages.JoinFile("sitecustomize.py"); |
||||
file.Directory?.Create(); |
||||
await Assets.PyScriptSiteCustomize.ExtractTo(file, true); |
||||
} |
||||
} |
@ -0,0 +1,403 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> |
||||
<Design.PreviewWith> |
||||
<Border Padding="20"> |
||||
<ui:CommandBar |
||||
Margin="8" |
||||
VerticalAlignment="Center" |
||||
VerticalContentAlignment="Center" |
||||
HorizontalAlignment="Left" |
||||
HorizontalContentAlignment="Left" |
||||
DefaultLabelPosition="Right"> |
||||
<ui:CommandBar.PrimaryCommands> |
||||
<ui:CommandBarButton Classes="success" Label="Success Button" Margin="8" |
||||
HorizontalAlignment="Center" /> |
||||
<ui:CommandBarButton Classes="accent" Label="FA Accent Button" Margin="8" |
||||
HorizontalAlignment="Center" /> |
||||
<ui:CommandBarButton Classes="systemaccent" Label="System Accent Button" Margin="8" |
||||
HorizontalAlignment="Center" /> |
||||
<ui:CommandBarButton Classes="danger" Label="Danger Button" Margin="8" HorizontalAlignment="Center" /> |
||||
<ui:CommandBarButton Classes="info" Label="Info Button" Margin="8" HorizontalAlignment="Center" /> |
||||
<ui:CommandBarButton Classes="transparent-info" Label="Semi-Transparent Info Button" Margin="8" |
||||
HorizontalAlignment="Center" /> |
||||
<ui:CommandBarButton Classes="transparent" Label="Transparent Button" Margin="8" |
||||
HorizontalAlignment="Center" /> |
||||
<ui:CommandBarButton Classes="transparent-full" Label="Transparent Button" Margin="8" |
||||
HorizontalAlignment="Center" /> |
||||
<ui:CommandBarButton Label="Disabled Button" Margin="8" IsEnabled="False" |
||||
HorizontalAlignment="Center" /> |
||||
</ui:CommandBar.PrimaryCommands> |
||||
</ui:CommandBar> |
||||
</Border> |
||||
</Design.PreviewWith> |
||||
|
||||
<!-- Success --> |
||||
<Style Selector="ui|CommandBarButton.success"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeGreenColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeGreenColor}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ TextBlock#TextLabel"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkGreenColor}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkGreenColor}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="Green" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkDarkGreenColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkDarkGreenColor}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- Danger --> |
||||
<Style Selector="ui|CommandBarButton.danger"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeRedColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeRedColor}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkRedColor}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkRedColor}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkDarkRedColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkDarkRedColor}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- Info --> |
||||
<Style Selector="ui|CommandBarButton.info"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeLightBlueColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeLightBlueColor}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeBlueColor}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBlueColor}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkBlueColor}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!--Accent Button--> |
||||
<Style Selector="ui|CommandBarButton.accent"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPointerOver}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPressed}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- SystemAccent --> |
||||
<Style Selector="ui|CommandBarButton.systemaccent"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColorDark1}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark1}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColorDark2}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark2}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- Transparent --> |
||||
<Style Selector="ui|CommandBarButton.transparent"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- Semi-Transparent Info --> |
||||
<Style Selector="ui|CommandBarButton.transparent-info"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeLightBlueColorTransparent}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeLightBlueColorTransparent}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeBlueColorTransparent}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBlueColorTransparent}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColorTransparent}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkBlueColorTransparent}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- Transparent red --> |
||||
<Style Selector="ui|CommandBarButton.transparent-red"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeCoralRedColor}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeCoralRedColor}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ThemeDarkCoralRedColor}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkCoralRedColor}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!-- Full Transparent --> |
||||
<Style Selector="ui|CommandBarButton.transparent-full"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<Style Selector="^:disabled"> |
||||
<Style Selector="^ /template/ Border#AppBarButtonInnerBorder"> |
||||
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlTransparentBrush}" /> |
||||
</Style> |
||||
<Style Selector="^"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> |
||||
</Style> |
||||
</Style> |
||||
</Style> |
||||
</Styles> |
@ -0,0 +1,185 @@
|
||||
using System.Collections.Generic; |
||||
using System.Collections.ObjectModel; |
||||
using System.ComponentModel; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using AsyncAwaitBestPractices; |
||||
using Avalonia.Controls; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Avalonia.Views.Dialogs; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Helper.Factory; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Api.OpenArt; |
||||
using StabilityMatrix.Core.Models.Packages.Extensions; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; |
||||
|
||||
[View(typeof(OpenArtWorkflowDialog))] |
||||
[ManagedService] |
||||
[Transient] |
||||
public partial class OpenArtWorkflowViewModel( |
||||
ISettingsManager settingsManager, |
||||
IPackageFactory packageFactory |
||||
) : ContentDialogViewModelBase |
||||
{ |
||||
public required OpenArtSearchResult Workflow { get; init; } |
||||
|
||||
[ObservableProperty] |
||||
private ObservableCollection<OpenArtCustomNode> customNodes = []; |
||||
|
||||
[ObservableProperty] |
||||
private string prunedDescription = string.Empty; |
||||
|
||||
[ObservableProperty] |
||||
private bool installRequiredNodes = true; |
||||
|
||||
[ObservableProperty] |
||||
private InstalledPackage? selectedPackage; |
||||
|
||||
public PackagePair? SelectedPackagePair => |
||||
SelectedPackage is { } package ? packageFactory.GetPackagePair(package) : null; |
||||
|
||||
public List<InstalledPackage> AvailablePackages => |
||||
settingsManager |
||||
.Settings.InstalledPackages.Where(package => package.PackageName == "ComfyUI") |
||||
.ToList(); |
||||
|
||||
public List<PackageExtension> MissingNodes { get; } = []; |
||||
|
||||
public override async Task OnLoadedAsync() |
||||
{ |
||||
if (Design.IsDesignMode) |
||||
return; |
||||
|
||||
if (settingsManager.Settings.PreferredWorkflowPackage is { } preferredPackage) |
||||
{ |
||||
SelectedPackage = preferredPackage; |
||||
} |
||||
else |
||||
{ |
||||
SelectedPackage = AvailablePackages.FirstOrDefault(); |
||||
} |
||||
|
||||
if (SelectedPackage == null) |
||||
{ |
||||
InstallRequiredNodes = false; |
||||
} |
||||
|
||||
CustomNodes = new ObservableCollection<OpenArtCustomNode>( |
||||
await ParseNodes(Workflow.NodesIndex.ToList()) |
||||
); |
||||
PrunedDescription = Utilities.RemoveHtml(Workflow.Description); |
||||
} |
||||
|
||||
partial void OnSelectedPackageChanged(InstalledPackage? oldValue, InstalledPackage? newValue) |
||||
{ |
||||
if (oldValue is null) |
||||
return; |
||||
|
||||
settingsManager.Transaction(settings => |
||||
{ |
||||
settings.PreferredWorkflowPackage = newValue; |
||||
}); |
||||
|
||||
OnLoadedAsync().SafeFireAndForget(); |
||||
} |
||||
|
||||
[Localizable(false)] |
||||
private async Task<List<OpenArtCustomNode>> ParseNodes(List<string> nodes) |
||||
{ |
||||
var indexOfFirstDot = nodes.IndexOf("."); |
||||
if (indexOfFirstDot != -1) |
||||
{ |
||||
nodes = nodes[(indexOfFirstDot + 1)..]; |
||||
} |
||||
|
||||
var installedNodesNames = new HashSet<string>(); |
||||
var nameToManifestNodes = new Dictionary<string, PackageExtension>(); |
||||
|
||||
var packagePair = SelectedPackagePair; |
||||
|
||||
if (packagePair?.BasePackage.ExtensionManager is { } extensionManager) |
||||
{ |
||||
var installedNodes = ( |
||||
await extensionManager.GetInstalledExtensionsLiteAsync(packagePair.InstalledPackage) |
||||
).ToList(); |
||||
|
||||
var manifestExtensionsMap = await extensionManager.GetManifestExtensionsMapAsync( |
||||
extensionManager.GetManifests(packagePair.InstalledPackage) |
||||
); |
||||
|
||||
// Add manifestExtensions definition to installedNodes if matching git repository url |
||||
installedNodes = installedNodes |
||||
.Select(installedNode => |
||||
{ |
||||
if ( |
||||
installedNode.GitRepositoryUrl is not null |
||||
&& manifestExtensionsMap.TryGetValue( |
||||
installedNode.GitRepositoryUrl, |
||||
out var manifestExtension |
||||
) |
||||
) |
||||
{ |
||||
installedNode = installedNode with { Definition = manifestExtension }; |
||||
} |
||||
|
||||
return installedNode; |
||||
}) |
||||
.ToList(); |
||||
|
||||
// There may be duplicate titles, deduplicate by using the first one |
||||
nameToManifestNodes = manifestExtensionsMap |
||||
.GroupBy(x => x.Value.Title) |
||||
.ToDictionary(x => x.Key, x => x.First().Value); |
||||
|
||||
installedNodesNames = installedNodes.Select(x => x.Title).ToHashSet(); |
||||
} |
||||
|
||||
var sections = new List<OpenArtCustomNode>(); |
||||
OpenArtCustomNode? currentSection = null; |
||||
|
||||
foreach (var node in nodes) |
||||
{ |
||||
if (node is "." or ",") |
||||
{ |
||||
currentSection = null; // End of the current section |
||||
continue; |
||||
} |
||||
|
||||
if (currentSection == null) |
||||
{ |
||||
currentSection = new OpenArtCustomNode |
||||
{ |
||||
Title = node, |
||||
IsInstalled = installedNodesNames.Contains(node) |
||||
}; |
||||
|
||||
// Add missing nodes to the list |
||||
if ( |
||||
!currentSection.IsInstalled && nameToManifestNodes.TryGetValue(node, out var manifestNode) |
||||
) |
||||
{ |
||||
MissingNodes.Add(manifestNode); |
||||
} |
||||
|
||||
sections.Add(currentSection); |
||||
} |
||||
else |
||||
{ |
||||
currentSection.Children.Add(node); |
||||
} |
||||
} |
||||
|
||||
if (sections.FirstOrDefault(x => x.Title == "ComfyUI") != null) |
||||
{ |
||||
sections = sections.Where(x => x.Title != "ComfyUI").ToList(); |
||||
} |
||||
|
||||
return sections; |
||||
} |
||||
} |
@ -0,0 +1,168 @@
|
||||
using System; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Text.Json; |
||||
using System.Threading.Tasks; |
||||
using AsyncAwaitBestPractices; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Platform.Storage; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using DynamicData; |
||||
using DynamicData.Binding; |
||||
using FluentAvalonia.UI.Controls; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.Languages; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
using StabilityMatrix.Avalonia.Services; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Avalonia.Views; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Models.Api.OpenArt; |
||||
using StabilityMatrix.Core.Processes; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
|
||||
[View(typeof(InstalledWorkflowsPage))] |
||||
[Singleton] |
||||
public partial class InstalledWorkflowsViewModel( |
||||
ISettingsManager settingsManager, |
||||
INotificationService notificationService |
||||
) : TabViewModelBase, IDisposable |
||||
{ |
||||
public override string Header => Resources.TabLabel_InstalledWorkflows; |
||||
|
||||
private readonly SourceCache<OpenArtMetadata, string> workflowsCache = |
||||
new(x => x.Workflow?.Id ?? Guid.NewGuid().ToString()); |
||||
|
||||
[ObservableProperty] |
||||
private IObservableCollection<OpenArtMetadata> displayedWorkflows = |
||||
new ObservableCollectionExtended<OpenArtMetadata>(); |
||||
|
||||
protected override async Task OnInitialLoadedAsync() |
||||
{ |
||||
await base.OnInitialLoadedAsync(); |
||||
|
||||
workflowsCache.Connect().DeferUntilLoaded().Bind(DisplayedWorkflows).Subscribe(); |
||||
|
||||
if (Design.IsDesignMode) |
||||
return; |
||||
|
||||
await LoadInstalledWorkflowsAsync(); |
||||
EventManager.Instance.WorkflowInstalled += OnWorkflowInstalled; |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task LoadInstalledWorkflowsAsync() |
||||
{ |
||||
workflowsCache.Clear(); |
||||
|
||||
foreach ( |
||||
var workflowPath in Directory.EnumerateFiles( |
||||
settingsManager.WorkflowDirectory, |
||||
"*.json", |
||||
SearchOption.AllDirectories |
||||
) |
||||
) |
||||
{ |
||||
try |
||||
{ |
||||
var json = await File.ReadAllTextAsync(workflowPath); |
||||
var metadata = JsonSerializer.Deserialize<OpenArtMetadata>(json); |
||||
|
||||
if (metadata?.Workflow == null) |
||||
{ |
||||
metadata = new OpenArtMetadata |
||||
{ |
||||
Workflow = new OpenArtSearchResult |
||||
{ |
||||
Id = Guid.NewGuid().ToString(), |
||||
Name = Path.GetFileNameWithoutExtension(workflowPath) |
||||
} |
||||
}; |
||||
} |
||||
|
||||
metadata.FilePath = [await App.StorageProvider.TryGetFileFromPathAsync(workflowPath)]; |
||||
workflowsCache.AddOrUpdate(metadata); |
||||
} |
||||
catch (Exception e) |
||||
{ |
||||
Console.WriteLine(e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task OpenInExplorer(OpenArtMetadata metadata) |
||||
{ |
||||
if (metadata.FilePath == null) |
||||
return; |
||||
|
||||
var path = metadata.FilePath.FirstOrDefault()?.Path.ToString(); |
||||
if (string.IsNullOrWhiteSpace(path)) |
||||
return; |
||||
|
||||
await ProcessRunner.OpenFileBrowser(path); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private void OpenOnOpenArt(OpenArtMetadata metadata) |
||||
{ |
||||
if (metadata.Workflow == null) |
||||
return; |
||||
|
||||
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{metadata.Workflow.Id}"); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task DeleteAsync(OpenArtMetadata metadata) |
||||
{ |
||||
var confirmationDialog = new BetterContentDialog |
||||
{ |
||||
Title = Resources.Label_AreYouSure, |
||||
Content = Resources.Label_ActionCannotBeUndone, |
||||
PrimaryButtonText = Resources.Action_Delete, |
||||
SecondaryButtonText = Resources.Action_Cancel, |
||||
DefaultButton = ContentDialogButton.Primary, |
||||
IsSecondaryButtonEnabled = true, |
||||
}; |
||||
var dialogResult = await confirmationDialog.ShowAsync(); |
||||
if (dialogResult != ContentDialogResult.Primary) |
||||
return; |
||||
|
||||
await using var delay = new MinimumDelay(200, 500); |
||||
|
||||
var path = metadata?.FilePath?.FirstOrDefault()?.Path.ToString().Replace("file:///", ""); |
||||
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) |
||||
{ |
||||
await notificationService.TryAsync( |
||||
Task.Run(() => File.Delete(path)), |
||||
message: "Error deleting workflow" |
||||
); |
||||
|
||||
var id = metadata?.Workflow?.Id; |
||||
if (!string.IsNullOrWhiteSpace(id)) |
||||
{ |
||||
workflowsCache.Remove(id); |
||||
} |
||||
} |
||||
|
||||
notificationService.Show( |
||||
Resources.Label_WorkflowDeleted, |
||||
string.Format(Resources.Label_WorkflowDeletedSuccessfully, metadata?.Workflow?.Name) |
||||
); |
||||
} |
||||
|
||||
private void OnWorkflowInstalled(object? sender, EventArgs e) |
||||
{ |
||||
LoadInstalledWorkflowsAsync().SafeFireAndForget(); |
||||
} |
||||
|
||||
public void Dispose() |
||||
{ |
||||
workflowsCache.Dispose(); |
||||
EventManager.Instance.WorkflowInstalled -= OnWorkflowInstalled; |
||||
} |
||||
} |
@ -0,0 +1,348 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.ComponentModel; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using AsyncAwaitBestPractices; |
||||
using Avalonia.Controls.Notifications; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using DynamicData; |
||||
using DynamicData.Binding; |
||||
using FluentAvalonia.UI.Controls; |
||||
using Refit; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.Languages; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
using StabilityMatrix.Avalonia.Services; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Avalonia.ViewModels.Dialogs; |
||||
using StabilityMatrix.Avalonia.Views; |
||||
using StabilityMatrix.Core.Api; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Helper.Factory; |
||||
using StabilityMatrix.Core.Models.Api.OpenArt; |
||||
using StabilityMatrix.Core.Models.PackageModification; |
||||
using StabilityMatrix.Core.Processes; |
||||
using StabilityMatrix.Core.Services; |
||||
using Resources = StabilityMatrix.Avalonia.Languages.Resources; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
|
||||
[View(typeof(OpenArtBrowserPage))] |
||||
[Singleton] |
||||
public partial class OpenArtBrowserViewModel( |
||||
IOpenArtApi openArtApi, |
||||
INotificationService notificationService, |
||||
ISettingsManager settingsManager, |
||||
IPackageFactory packageFactory, |
||||
ServiceManager<ViewModelBase> vmFactory |
||||
) : TabViewModelBase, IInfinitelyScroll |
||||
{ |
||||
private const int PageSize = 20; |
||||
|
||||
public override string Header => Resources.Label_OpenArtBrowser; |
||||
|
||||
private readonly SourceCache<OpenArtSearchResult, string> searchResultsCache = new(x => x.Id); |
||||
|
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(PageCount), nameof(CanGoBack), nameof(CanGoForward), nameof(CanGoToEnd))] |
||||
private OpenArtSearchResponse? latestSearchResponse; |
||||
|
||||
[ObservableProperty] |
||||
private IObservableCollection<OpenArtSearchResult> searchResults = |
||||
new ObservableCollectionExtended<OpenArtSearchResult>(); |
||||
|
||||
[ObservableProperty] |
||||
private string searchQuery = string.Empty; |
||||
|
||||
[ObservableProperty] |
||||
private bool isLoading; |
||||
|
||||
[ObservableProperty] |
||||
[NotifyPropertyChangedFor(nameof(InternalPageNumber), nameof(CanGoBack))] |
||||
private int displayedPageNumber = 1; |
||||
|
||||
public int InternalPageNumber => DisplayedPageNumber - 1; |
||||
|
||||
public int PageCount => |
||||
Math.Max( |
||||
1, |
||||
Convert.ToInt32(Math.Ceiling((LatestSearchResponse?.Total ?? 0) / Convert.ToDouble(PageSize))) |
||||
); |
||||
|
||||
public bool CanGoBack => |
||||
string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && InternalPageNumber > 0; |
||||
|
||||
public bool CanGoForward => |
||||
!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) || PageCount > InternalPageNumber + 1; |
||||
|
||||
public bool CanGoToEnd => |
||||
string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && PageCount > InternalPageNumber + 1; |
||||
|
||||
public IEnumerable<string> AllSortModes => ["Trending", "Latest", "Most Downloaded", "Most Liked"]; |
||||
|
||||
[ObservableProperty] |
||||
private string? selectedSortMode; |
||||
|
||||
protected override void OnInitialLoaded() |
||||
{ |
||||
searchResultsCache.Connect().DeferUntilLoaded().Bind(SearchResults).Subscribe(); |
||||
SelectedSortMode = AllSortModes.First(); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task FirstPage() |
||||
{ |
||||
DisplayedPageNumber = 1; |
||||
searchResultsCache.Clear(); |
||||
|
||||
await DoSearch(); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task PreviousPage() |
||||
{ |
||||
DisplayedPageNumber--; |
||||
searchResultsCache.Clear(); |
||||
|
||||
await DoSearch(InternalPageNumber); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task NextPage() |
||||
{ |
||||
if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) |
||||
{ |
||||
DisplayedPageNumber++; |
||||
} |
||||
|
||||
searchResultsCache.Clear(); |
||||
await DoSearch(InternalPageNumber); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task LastPage() |
||||
{ |
||||
if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) |
||||
{ |
||||
DisplayedPageNumber = PageCount; |
||||
} |
||||
|
||||
searchResultsCache.Clear(); |
||||
await DoSearch(PageCount - 1); |
||||
} |
||||
|
||||
[Localizable(false)] |
||||
[RelayCommand] |
||||
private void OpenModel(OpenArtSearchResult workflow) |
||||
{ |
||||
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}"); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task SearchButton() |
||||
{ |
||||
DisplayedPageNumber = 1; |
||||
LatestSearchResponse = null; |
||||
searchResultsCache.Clear(); |
||||
|
||||
await DoSearch(); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task OpenWorkflow(OpenArtSearchResult workflow) |
||||
{ |
||||
var vm = new OpenArtWorkflowViewModel(settingsManager, packageFactory) { Workflow = workflow }; |
||||
|
||||
var dialog = new BetterContentDialog |
||||
{ |
||||
IsPrimaryButtonEnabled = true, |
||||
IsSecondaryButtonEnabled = true, |
||||
PrimaryButtonText = Resources.Action_Import, |
||||
SecondaryButtonText = Resources.Action_Cancel, |
||||
DefaultButton = ContentDialogButton.Primary, |
||||
IsFooterVisible = true, |
||||
MaxDialogWidth = 750, |
||||
MaxDialogHeight = 850, |
||||
CloseOnClickOutside = true, |
||||
Content = vm |
||||
}; |
||||
|
||||
var result = await dialog.ShowAsync(); |
||||
|
||||
if (result != ContentDialogResult.Primary) |
||||
return; |
||||
|
||||
List<IPackageStep> steps = |
||||
[ |
||||
new DownloadOpenArtWorkflowStep(openArtApi, vm.Workflow, settingsManager) |
||||
]; |
||||
|
||||
// Add install steps if missing nodes and preferred |
||||
if ( |
||||
vm is |
||||
{ |
||||
InstallRequiredNodes: true, |
||||
MissingNodes: { Count: > 0 } missingNodes, |
||||
SelectedPackage: not null, |
||||
SelectedPackagePair: not null |
||||
} |
||||
) |
||||
{ |
||||
var extensionManager = vm.SelectedPackagePair.BasePackage.ExtensionManager!; |
||||
|
||||
steps.AddRange( |
||||
missingNodes.Select( |
||||
extension => |
||||
new InstallExtensionStep( |
||||
extensionManager, |
||||
vm.SelectedPackagePair.InstalledPackage, |
||||
extension |
||||
) |
||||
) |
||||
); |
||||
} |
||||
|
||||
var runner = new PackageModificationRunner |
||||
{ |
||||
ShowDialogOnStart = true, |
||||
ModificationCompleteTitle = Resources.Label_WorkflowImported, |
||||
ModificationCompleteMessage = Resources.Label_FinishedImportingWorkflow |
||||
}; |
||||
EventManager.Instance.OnPackageInstallProgressAdded(runner); |
||||
|
||||
await runner.ExecuteSteps(steps); |
||||
|
||||
notificationService.Show( |
||||
Resources.Label_WorkflowImported, |
||||
Resources.Label_WorkflowImportComplete, |
||||
NotificationType.Success |
||||
); |
||||
|
||||
EventManager.Instance.OnWorkflowInstalled(); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private void OpenOnOpenArt(OpenArtSearchResult? workflow) |
||||
{ |
||||
if (workflow?.Id == null) |
||||
return; |
||||
|
||||
ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}"); |
||||
} |
||||
|
||||
private async Task DoSearch(int page = 0) |
||||
{ |
||||
IsLoading = true; |
||||
|
||||
try |
||||
{ |
||||
OpenArtSearchResponse? response = null; |
||||
if (string.IsNullOrWhiteSpace(SearchQuery)) |
||||
{ |
||||
var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) }; |
||||
if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) |
||||
{ |
||||
request.Cursor = LatestSearchResponse.NextCursor; |
||||
} |
||||
|
||||
response = await openArtApi.GetFeedAsync(request); |
||||
} |
||||
else |
||||
{ |
||||
response = await openArtApi.SearchAsync( |
||||
new OpenArtSearchRequest |
||||
{ |
||||
Keyword = SearchQuery, |
||||
PageSize = PageSize, |
||||
CurrentPage = page |
||||
} |
||||
); |
||||
} |
||||
|
||||
foreach (var item in response.Items) |
||||
{ |
||||
searchResultsCache.AddOrUpdate(item); |
||||
} |
||||
|
||||
LatestSearchResponse = response; |
||||
} |
||||
catch (ApiException e) |
||||
{ |
||||
notificationService.Show(Resources.Label_ErrorRetrievingWorkflows, e.Message); |
||||
} |
||||
finally |
||||
{ |
||||
IsLoading = false; |
||||
} |
||||
} |
||||
|
||||
partial void OnSelectedSortModeChanged(string? value) |
||||
{ |
||||
if (value is null || SearchResults.Count == 0) |
||||
return; |
||||
|
||||
searchResultsCache.Clear(); |
||||
LatestSearchResponse = null; |
||||
|
||||
DoSearch().SafeFireAndForget(); |
||||
} |
||||
|
||||
public async Task LoadNextPageAsync() |
||||
{ |
||||
if (!CanGoForward) |
||||
return; |
||||
|
||||
try |
||||
{ |
||||
OpenArtSearchResponse? response = null; |
||||
if (string.IsNullOrWhiteSpace(SearchQuery)) |
||||
{ |
||||
var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) }; |
||||
if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) |
||||
{ |
||||
request.Cursor = LatestSearchResponse.NextCursor; |
||||
} |
||||
|
||||
response = await openArtApi.GetFeedAsync(request); |
||||
} |
||||
else |
||||
{ |
||||
DisplayedPageNumber++; |
||||
response = await openArtApi.SearchAsync( |
||||
new OpenArtSearchRequest |
||||
{ |
||||
Keyword = SearchQuery, |
||||
PageSize = PageSize, |
||||
CurrentPage = InternalPageNumber |
||||
} |
||||
); |
||||
} |
||||
|
||||
foreach (var item in response.Items) |
||||
{ |
||||
searchResultsCache.AddOrUpdate(item); |
||||
} |
||||
|
||||
LatestSearchResponse = response; |
||||
} |
||||
catch (ApiException e) |
||||
{ |
||||
notificationService.Show("Unable to load the next page", e.Message); |
||||
} |
||||
} |
||||
|
||||
private static string GetSortMode(string? sortMode) |
||||
{ |
||||
return sortMode switch |
||||
{ |
||||
"Trending" => "trending", |
||||
"Latest" => "latest", |
||||
"Most Downloaded" => "most_downloaded", |
||||
"Most Liked" => "most_liked", |
||||
_ => "trending" |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,161 @@
|
||||
using System; |
||||
using System.Threading.Tasks; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using FluentAvalonia.UI.Controls; |
||||
using StabilityMatrix.Avalonia.Services; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Avalonia.Views; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Packages; |
||||
using StabilityMatrix.Core.Processes; |
||||
using StabilityMatrix.Core.Services; |
||||
using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; |
||||
using TeachingTip = StabilityMatrix.Core.Models.Settings.TeachingTip; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
|
||||
[View(typeof(ConsoleOutputPage))] |
||||
public partial class RunningPackageViewModel : PageViewModelBase, IDisposable, IAsyncDisposable |
||||
{ |
||||
private readonly ISettingsManager settingsManager; |
||||
private readonly INotificationService notificationService; |
||||
private readonly RunningPackageService runningPackageService; |
||||
|
||||
public PackagePair RunningPackage { get; } |
||||
public ConsoleViewModel Console { get; } |
||||
public override string Title => RunningPackage.InstalledPackage.PackageName ?? "Running Package"; |
||||
public override IconSource IconSource => new SymbolIconSource(); |
||||
|
||||
[ObservableProperty] |
||||
private bool autoScrollToEnd; |
||||
|
||||
[ObservableProperty] |
||||
private bool showWebUiButton; |
||||
|
||||
[ObservableProperty] |
||||
private string webUiUrl = string.Empty; |
||||
|
||||
[ObservableProperty] |
||||
private bool isRunning = true; |
||||
|
||||
[ObservableProperty] |
||||
private string consoleInput = string.Empty; |
||||
|
||||
[ObservableProperty] |
||||
private bool showWebUiTeachingTip; |
||||
|
||||
/// <inheritdoc/> |
||||
public RunningPackageViewModel( |
||||
ISettingsManager settingsManager, |
||||
INotificationService notificationService, |
||||
RunningPackageService runningPackageService, |
||||
PackagePair runningPackage, |
||||
ConsoleViewModel console |
||||
) |
||||
{ |
||||
this.settingsManager = settingsManager; |
||||
this.notificationService = notificationService; |
||||
this.runningPackageService = runningPackageService; |
||||
|
||||
RunningPackage = runningPackage; |
||||
Console = console; |
||||
Console.Document.LineCountChanged += DocumentOnLineCountChanged; |
||||
RunningPackage.BasePackage.StartupComplete += BasePackageOnStartupComplete; |
||||
RunningPackage.BasePackage.Exited += BasePackageOnExited; |
||||
|
||||
settingsManager.RelayPropertyFor( |
||||
this, |
||||
vm => vm.AutoScrollToEnd, |
||||
settings => settings.AutoScrollLaunchConsoleToEnd, |
||||
true |
||||
); |
||||
} |
||||
|
||||
private void BasePackageOnExited(object? sender, int e) |
||||
{ |
||||
IsRunning = false; |
||||
ShowWebUiButton = false; |
||||
Console.Document.LineCountChanged -= DocumentOnLineCountChanged; |
||||
RunningPackage.BasePackage.StartupComplete -= BasePackageOnStartupComplete; |
||||
RunningPackage.BasePackage.Exited -= BasePackageOnExited; |
||||
runningPackageService.RunningPackages.Remove(RunningPackage.InstalledPackage.Id); |
||||
} |
||||
|
||||
private void BasePackageOnStartupComplete(object? sender, string url) |
||||
{ |
||||
WebUiUrl = url.Replace("0.0.0.0", "127.0.0.1"); |
||||
ShowWebUiButton = !string.IsNullOrWhiteSpace(WebUiUrl); |
||||
|
||||
if (settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.WebUiButtonMovedTip)) |
||||
return; |
||||
|
||||
ShowWebUiTeachingTip = true; |
||||
settingsManager.Transaction(s => s.SeenTeachingTips.Add(TeachingTip.WebUiButtonMovedTip)); |
||||
} |
||||
|
||||
private void DocumentOnLineCountChanged(object? sender, EventArgs e) |
||||
{ |
||||
if (AutoScrollToEnd) |
||||
{ |
||||
EventManager.Instance.OnScrollToBottomRequested(); |
||||
} |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private void LaunchPackage() |
||||
{ |
||||
EventManager.Instance.OnPackageRelaunchRequested(RunningPackage.InstalledPackage); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task Stop() |
||||
{ |
||||
await runningPackageService.StopPackage(RunningPackage.InstalledPackage.Id); |
||||
Console.PostLine($"{Environment.NewLine}Stopped process at {DateTimeOffset.Now}"); |
||||
await Console.StopUpdatesAsync(); |
||||
IsRunning = false; |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private void LaunchWebUi() |
||||
{ |
||||
if (string.IsNullOrEmpty(WebUiUrl)) |
||||
return; |
||||
|
||||
notificationService.TryAsync( |
||||
Task.Run(() => ProcessRunner.OpenUrl(WebUiUrl)), |
||||
"Failed to open URL", |
||||
$"{WebUiUrl}" |
||||
); |
||||
} |
||||
|
||||
[RelayCommand] |
||||
private async Task SendToConsole() |
||||
{ |
||||
Console.PostLine(ConsoleInput); |
||||
if (RunningPackage?.BasePackage is BaseGitPackage gitPackage) |
||||
{ |
||||
var venv = gitPackage.VenvRunner; |
||||
var process = venv?.Process; |
||||
if (process is not null) |
||||
{ |
||||
await process.StandardInput.WriteLineAsync(ConsoleInput); |
||||
} |
||||
} |
||||
|
||||
ConsoleInput = string.Empty; |
||||
} |
||||
|
||||
public void Dispose() |
||||
{ |
||||
Console.Dispose(); |
||||
} |
||||
|
||||
public async ValueTask DisposeAsync() |
||||
{ |
||||
await Console.DisposeAsync(); |
||||
} |
||||
} |
@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using Avalonia.Controls; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using FluentAvalonia.UI.Controls; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.Languages; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Avalonia.Views; |
||||
using StabilityMatrix.Core.Attributes; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
|
||||
[View(typeof(WorkflowsPage))] |
||||
[Singleton] |
||||
public partial class WorkflowsPageViewModel : PageViewModelBase |
||||
{ |
||||
public override string Title => Resources.Label_Workflows; |
||||
public override IconSource IconSource => new FASymbolIconSource { Symbol = "fa-solid fa-circle-nodes" }; |
||||
|
||||
public IReadOnlyList<TabItem> Pages { get; } |
||||
|
||||
[ObservableProperty] |
||||
private TabItem? selectedPage; |
||||
|
||||
/// <inheritdoc/> |
||||
public WorkflowsPageViewModel( |
||||
OpenArtBrowserViewModel openArtBrowserViewModel, |
||||
InstalledWorkflowsViewModel installedWorkflowsViewModel |
||||
) |
||||
{ |
||||
Pages = new List<TabItem>( |
||||
new List<TabViewModelBase>([openArtBrowserViewModel, installedWorkflowsViewModel]).Select( |
||||
vm => new TabItem { Header = vm.Header, Content = vm } |
||||
) |
||||
); |
||||
SelectedPage = Pages.FirstOrDefault(); |
||||
} |
||||
} |
@ -0,0 +1,93 @@
|
||||
<controls:UserControlBase xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit" |
||||
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" |
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" |
||||
xmlns:avalonia="https://github.com/projektanker/icons.avalonia" |
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
x:DataType="viewModels:RunningPackageViewModel" |
||||
x:Class="StabilityMatrix.Avalonia.Views.ConsoleOutputPage"> |
||||
<Grid RowDefinitions="Auto, *, Auto"> |
||||
<ui:CommandBar Grid.Row="0" |
||||
Margin="12,0,0,4" |
||||
VerticalAlignment="Center" |
||||
VerticalContentAlignment="Center" |
||||
HorizontalAlignment="Left" |
||||
HorizontalContentAlignment="Left" |
||||
DefaultLabelPosition="Right"> |
||||
<ui:CommandBar.PrimaryCommands> |
||||
<ui:CommandBarButton |
||||
IconSource="Stop" |
||||
IsVisible="{Binding IsRunning}" |
||||
Classes="danger" |
||||
Command="{Binding StopCommand}" |
||||
VerticalAlignment="Center" |
||||
Label="{x:Static lang:Resources.Action_Stop}" /> |
||||
<ui:CommandBarButton |
||||
IconSource="Play" |
||||
IsVisible="{Binding !IsRunning}" |
||||
Classes="success" |
||||
Command="{Binding LaunchPackageCommand}" |
||||
VerticalAlignment="Center" |
||||
Label="{x:Static lang:Resources.Action_Launch}" /> |
||||
|
||||
<ui:CommandBarSeparator Margin="6,0,4,0"/> |
||||
|
||||
<ui:CommandBarToggleButton |
||||
VerticalAlignment="Center" |
||||
IsChecked="{Binding AutoScrollToEnd}" |
||||
Label="{x:Static lang:Resources.Label_ToggleAutoScrolling}" |
||||
ToolTip.Tip="{x:Static lang:Resources.Label_AutoScrollToEnd}"> |
||||
<ui:CommandBarToggleButton.IconSource> |
||||
<controls:FASymbolIconSource Symbol="fa-solid fa-arrow-down-wide-short" /> |
||||
</ui:CommandBarToggleButton.IconSource> |
||||
</ui:CommandBarToggleButton> |
||||
|
||||
<ui:CommandBarButton |
||||
Command="{Binding LaunchWebUiCommand}" |
||||
IsVisible="{Binding ShowWebUiButton}" |
||||
VerticalAlignment="Center" |
||||
x:Name="OpenWebUiButton" |
||||
Label="{x:Static lang:Resources.Action_OpenWebUI}"> |
||||
<ui:CommandBarButton.IconSource> |
||||
<controls:FASymbolIconSource Symbol="fa-solid fa-up-right-from-square"/> |
||||
</ui:CommandBarButton.IconSource> |
||||
</ui:CommandBarButton> |
||||
</ui:CommandBar.PrimaryCommands> |
||||
</ui:CommandBar> |
||||
|
||||
<avaloniaEdit:TextEditor |
||||
Grid.Row="1" |
||||
x:Name="Console" |
||||
Margin="8,8,16,10" |
||||
DataContext="{Binding Console}" |
||||
Document="{Binding Document}" |
||||
FontFamily="Cascadia Code,Consolas,Menlo,Monospace" |
||||
IsReadOnly="True" |
||||
LineNumbersForeground="DarkSlateGray" |
||||
ShowLineNumbers="True" |
||||
VerticalScrollBarVisibility="Auto" |
||||
WordWrap="True" /> |
||||
|
||||
<Grid Grid.Row="2" ColumnDefinitions="*, Auto" |
||||
Margin="16,4,16,16"> |
||||
<TextBox Grid.Column="0" Text="{Binding ConsoleInput, Mode=TwoWay}" |
||||
Margin="0,0,8,0"/> |
||||
<Button Grid.Column="1" |
||||
Classes="accent" |
||||
IsDefault="True" |
||||
Content="{x:Static lang:Resources.Action_Send}" |
||||
Command="{Binding SendToConsoleCommand}"/> |
||||
</Grid> |
||||
|
||||
<ui:TeachingTip Grid.Row="0" Grid.Column="0" Name="TeachingTip1" |
||||
Target="{Binding #OpenWebUiButton}" |
||||
Title="{x:Static lang:Resources.TeachingTip_WebUiButtonMoved}" |
||||
PreferredPlacement="Bottom" |
||||
IsOpen="{Binding ShowWebUiTeachingTip}"/> |
||||
</Grid> |
||||
</controls:UserControlBase> |
@ -0,0 +1,54 @@
|
||||
using System; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Controls.Primitives; |
||||
using Avalonia.Interactivity; |
||||
using Avalonia.Threading; |
||||
using AvaloniaEdit; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.Helpers; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Helper; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Views; |
||||
|
||||
[Transient] |
||||
public partial class ConsoleOutputPage : UserControlBase |
||||
{ |
||||
private const int LineOffset = 5; |
||||
|
||||
public ConsoleOutputPage() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
||||
{ |
||||
base.OnApplyTemplate(e); |
||||
TextEditorConfigs.Configure(Console, TextEditorPreset.Console); |
||||
} |
||||
|
||||
protected override void OnUnloaded(RoutedEventArgs e) |
||||
{ |
||||
base.OnUnloaded(e); |
||||
EventManager.Instance.ScrollToBottomRequested -= OnScrollToBottomRequested; |
||||
} |
||||
|
||||
protected override void OnLoaded(RoutedEventArgs e) |
||||
{ |
||||
base.OnLoaded(e); |
||||
EventManager.Instance.ScrollToBottomRequested += OnScrollToBottomRequested; |
||||
} |
||||
|
||||
private void OnScrollToBottomRequested(object? sender, EventArgs e) |
||||
{ |
||||
Dispatcher.UIThread.Invoke(() => |
||||
{ |
||||
var editor = this.FindControl<TextEditor>("Console"); |
||||
if (editor?.Document == null) |
||||
return; |
||||
var line = Math.Max(editor.Document.LineCount - LineOffset, 1); |
||||
editor.ScrollToLine(line); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,136 @@
|
||||
<controls:UserControlBase |
||||
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.OpenArtWorkflowDialog" |
||||
xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:avalonia="https://github.com/projektanker/icons.avalonia" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs" |
||||
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models" |
||||
xmlns:system="clr-namespace:System;assembly=System.Runtime" |
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" |
||||
d:DataContext="{x:Static designData:DesignData.OpenArtWorkflowViewModel}" |
||||
d:DesignHeight="650" |
||||
d:DesignWidth="600" |
||||
x:DataType="dialogs:OpenArtWorkflowViewModel" |
||||
mc:Ignorable="d"> |
||||
<Grid |
||||
Width="600" |
||||
HorizontalAlignment="Stretch" |
||||
ColumnDefinitions="*, 2*" |
||||
RowDefinitions="Auto, Auto, Auto, Auto, Auto"> |
||||
<TextBlock |
||||
Grid.Row="0" |
||||
Grid.Column="0" |
||||
Grid.ColumnSpan="2" |
||||
Margin="8,8,0,4" |
||||
FontSize="20" |
||||
TextAlignment="Left" |
||||
ToolTip.Tip="{Binding Workflow.Name}"> |
||||
<Run Text="{Binding Workflow.Name}" /> |
||||
<Run Text="- by" /> |
||||
<Run Text="{Binding Workflow.Creator.Name}" /> |
||||
</TextBlock> |
||||
|
||||
<labs:AsyncImage |
||||
Grid.Row="2" |
||||
Grid.Column="0" |
||||
Height="300" |
||||
Margin="8" |
||||
CornerRadius="8" |
||||
Source="{Binding Workflow.Thumbnails[0].Url}" |
||||
Stretch="UniformToFill" /> |
||||
|
||||
<controls:Card |
||||
Grid.Row="2" |
||||
Grid.Column="1" |
||||
Margin="8" |
||||
VerticalAlignment="Top"> |
||||
<ScrollViewer MaxHeight="270"> |
||||
<TextBlock |
||||
Margin="4" |
||||
Text="{Binding PrunedDescription}" |
||||
TextWrapping="Wrap" /> |
||||
</ScrollViewer> |
||||
</controls:Card> |
||||
|
||||
<Expander |
||||
Grid.Row="3" |
||||
Grid.Column="0" |
||||
Grid.ColumnSpan="2" |
||||
Margin="8,8" |
||||
ExpandDirection="Down" |
||||
Header="{x:Static lang:Resources.Label_NodeDetails}"> |
||||
<ScrollViewer MaxHeight="225"> |
||||
<ItemsControl ItemsSource="{Binding CustomNodes}"> |
||||
<ItemsControl.ItemsPanel> |
||||
<ItemsPanelTemplate> |
||||
<StackPanel Orientation="Vertical" Spacing="4" /> |
||||
</ItemsPanelTemplate> |
||||
</ItemsControl.ItemsPanel> |
||||
<ItemsControl.ItemTemplate> |
||||
<DataTemplate DataType="{x:Type models:OpenArtCustomNode}"> |
||||
<StackPanel Orientation="Vertical"> |
||||
<StackPanel Orientation="Horizontal"> |
||||
<TextBlock |
||||
FontSize="16" |
||||
FontWeight="SemiBold" |
||||
Text="{Binding Title}" /> |
||||
<avalonia:Icon |
||||
Margin="4" |
||||
VerticalAlignment="Center" |
||||
Foreground="Lime" |
||||
IsVisible="{Binding IsInstalled}" |
||||
Value="fa-solid fa-circle-check" /> |
||||
</StackPanel> |
||||
<ItemsControl Margin="0,4" ItemsSource="{Binding Children}"> |
||||
<ItemsControl.ItemsPanel> |
||||
<ItemsPanelTemplate> |
||||
<StackPanel Orientation="Vertical" Spacing="4" /> |
||||
</ItemsPanelTemplate> |
||||
</ItemsControl.ItemsPanel> |
||||
<ItemsControl.ItemTemplate> |
||||
<DataTemplate DataType="{x:Type system:String}"> |
||||
<StackPanel Orientation="Vertical"> |
||||
<TextBlock Margin="4,0,0,0" Text="{Binding ., StringFormat={} - {0}}" /> |
||||
</StackPanel> |
||||
</DataTemplate> |
||||
</ItemsControl.ItemTemplate> |
||||
</ItemsControl> |
||||
</StackPanel> |
||||
</DataTemplate> |
||||
</ItemsControl.ItemTemplate> |
||||
</ItemsControl> |
||||
</ScrollViewer> |
||||
</Expander> |
||||
|
||||
<Expander |
||||
Grid.Row="4" |
||||
Grid.Column="0" |
||||
Grid.ColumnSpan="2" |
||||
Margin="8" |
||||
Header="Options"> |
||||
<StackPanel Spacing="4"> |
||||
<ui:SettingsExpanderItem Content="Install Required Nodes"> |
||||
<ui:SettingsExpanderItem.Footer> |
||||
<CheckBox IsChecked="{Binding InstallRequiredNodes}" |
||||
IsEnabled="{Binding AvailablePackages.Count}"/> |
||||
</ui:SettingsExpanderItem.Footer> |
||||
</ui:SettingsExpanderItem> |
||||
<ui:SettingsExpanderItem Content="Target Package"> |
||||
<ui:SettingsExpanderItem.Footer> |
||||
<ComboBox |
||||
DisplayMemberBinding="{Binding DisplayName}" |
||||
ItemsSource="{Binding AvailablePackages}" |
||||
SelectedItem="{Binding SelectedPackage}"/> |
||||
</ui:SettingsExpanderItem.Footer> |
||||
</ui:SettingsExpanderItem> |
||||
</StackPanel> |
||||
</Expander> |
||||
|
||||
</Grid> |
||||
</controls:UserControlBase> |
@ -0,0 +1,13 @@
|
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Core.Attributes; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Views.Dialogs; |
||||
|
||||
[Transient] |
||||
public partial class OpenArtWorkflowDialog : UserControlBase |
||||
{ |
||||
public OpenArtWorkflowDialog() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
} |
@ -0,0 +1,254 @@
|
||||
<controls:UserControlBase xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" |
||||
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" |
||||
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:openArt="clr-namespace:StabilityMatrix.Core.Models.Api.OpenArt" |
||||
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" |
||||
xmlns:avalonia="https://github.com/projektanker/icons.avalonia" |
||||
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" |
||||
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models" |
||||
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers" |
||||
xmlns:fluent="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent" |
||||
xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia" |
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
d:DataContext="{x:Static designData:DesignData.InstalledWorkflowsViewModel}" |
||||
x:DataType="viewModels:InstalledWorkflowsViewModel" |
||||
x:Class="StabilityMatrix.Avalonia.Views.InstalledWorkflowsPage"> |
||||
<UserControl.Styles> |
||||
<Style Selector="Border#HoverBorder"> |
||||
<Setter Property="Transitions"> |
||||
<Transitions> |
||||
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.237" /> |
||||
</Transitions> |
||||
</Setter> |
||||
|
||||
<Style Selector="^ labs|AsyncImage"> |
||||
<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> |
||||
<input:StandardUICommand |
||||
x:Key="OpenInExplorerCommand" |
||||
Command="{Binding OpenInExplorerCommand}" /> |
||||
<input:StandardUICommand |
||||
x:Key="OpenOnOpenArtCommand" |
||||
Command="{Binding OpenOnOpenArtCommand}" /> |
||||
<input:StandardUICommand |
||||
x:Key="DeleteCommand" |
||||
Command="{Binding DeleteCommand}" /> |
||||
</UserControl.Resources> |
||||
|
||||
<Grid RowDefinitions="Auto, *"> |
||||
<controls1:CommandBar Grid.Row="0" Grid.Column="0" |
||||
VerticalAlignment="Center" |
||||
HorizontalAlignment="Left" |
||||
VerticalContentAlignment="Center" |
||||
DefaultLabelPosition="Right"> |
||||
<controls1:CommandBar.PrimaryCommands> |
||||
<controls1:CommandBarButton |
||||
IconSource="Refresh" |
||||
VerticalAlignment="Center" |
||||
Label="{x:Static lang:Resources.Action_Refresh}" |
||||
Command="{Binding LoadInstalledWorkflowsCommand}" /> |
||||
|
||||
<controls1:CommandBarSeparator /> |
||||
|
||||
<controls1:CommandBarElementContainer> |
||||
<StackPanel Orientation="Horizontal"> |
||||
<avalonia:Icon FontSize="18" |
||||
Value="fa-solid fa-info" |
||||
Margin="8,0" /> |
||||
<TextBlock Text="Drag & drop one of the cards below into ComfyUI to load the workflow" |
||||
VerticalAlignment="Center" /> |
||||
</StackPanel> |
||||
</controls1:CommandBarElementContainer> |
||||
</controls1:CommandBar.PrimaryCommands> |
||||
</controls1:CommandBar> |
||||
|
||||
<ScrollViewer Grid.Column="0" |
||||
Grid.Row="1"> |
||||
<ItemsRepeater ItemsSource="{Binding DisplayedWorkflows}"> |
||||
<ItemsRepeater.Layout> |
||||
<!-- <UniformGridLayout MinColumnSpacing="4" MinRowSpacing="4"/> --> |
||||
<UniformGridLayout MinColumnSpacing="4" MinRowSpacing="4" /> |
||||
</ItemsRepeater.Layout> |
||||
<ItemsRepeater.ItemTemplate> |
||||
<DataTemplate x:DataType="{x:Type models:OpenArtMetadata}"> |
||||
<Border |
||||
Name="HoverBorder" |
||||
Padding="0" |
||||
BorderThickness="0" |
||||
Margin="8" |
||||
ClipToBounds="True" |
||||
CornerRadius="8"> |
||||
<Interaction.Behaviors> |
||||
<BehaviorCollection> |
||||
<controls:BetterContextDragBehavior |
||||
Context="{Binding FilePath}" |
||||
DataFormat="Files" |
||||
HorizontalDragThreshold="6" |
||||
VerticalDragThreshold="6" /> |
||||
</BehaviorCollection> |
||||
</Interaction.Behaviors> |
||||
<Border.ContextFlyout> |
||||
<MenuFlyout> |
||||
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnOpenArt}" |
||||
IsVisible="{Binding HasMetadata}" |
||||
Command="{StaticResource OpenOnOpenArtCommand}" |
||||
CommandParameter="{Binding }"> |
||||
<MenuItem.Icon> |
||||
<fluent:SymbolIcon Symbol="Open" /> |
||||
</MenuItem.Icon> |
||||
</MenuItem> |
||||
<MenuItem Header="{x:Static lang:Resources.Action_OpenInExplorer}" |
||||
Command="{StaticResource OpenInExplorerCommand}" |
||||
CommandParameter="{Binding }"> |
||||
<MenuItem.Icon> |
||||
<fluent:SymbolIcon Symbol="Folder" /> |
||||
</MenuItem.Icon> |
||||
</MenuItem> |
||||
<MenuItem Header="{x:Static lang:Resources.Action_Delete}" |
||||
Command="{StaticResource DeleteCommand}" |
||||
CommandParameter="{Binding }"> |
||||
<MenuItem.Icon> |
||||
<fluent:SymbolIcon Symbol="Delete" /> |
||||
</MenuItem.Icon> |
||||
</MenuItem> |
||||
</MenuFlyout> |
||||
</Border.ContextFlyout> |
||||
<Button |
||||
Name="ModelCard" |
||||
Classes="transparent-full" |
||||
Padding="0" |
||||
BorderThickness="0" |
||||
VerticalContentAlignment="Top" |
||||
CornerRadius="8"> |
||||
<Grid RowDefinitions="*, Auto"> |
||||
<labs:AsyncImage |
||||
Grid.Row="0" |
||||
Grid.RowSpan="2" |
||||
CornerRadius="8" |
||||
Width="330" |
||||
Height="400" |
||||
Source="{Binding FirstThumbnail}" |
||||
IsVisible="{Binding FirstThumbnail, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}" |
||||
Stretch="UniformToFill" /> |
||||
|
||||
<avalonia:Icon Grid.Row="0" |
||||
Grid.RowSpan="2" |
||||
CornerRadius="8" |
||||
Width="330" |
||||
Height="400" |
||||
FontSize="100" |
||||
IsVisible="{Binding FirstThumbnail, Converter={x:Static ObjectConverters.IsNull}, FallbackValue=False}" |
||||
Value="fa-regular fa-file-code" /> |
||||
|
||||
<!-- 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 Workflow.Creator.DevProfileUrl}" |
||||
CornerRadius="16" |
||||
Classes="transparent" |
||||
Padding="10,4"> |
||||
<StackPanel Orientation="Horizontal" Spacing="6"> |
||||
<labs:AsyncImage |
||||
Width="22" |
||||
Height="22" |
||||
Effect="{StaticResource ImageDropShadowEffect}" |
||||
CornerRadius="11" |
||||
RenderOptions.BitmapInterpolationMode="HighQuality" |
||||
IsVisible="{Binding Workflow.Creator.Avatar, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" |
||||
Source="{Binding Workflow.Creator.Avatar}" /> |
||||
<TextBlock |
||||
VerticalAlignment="Center" |
||||
Foreground="{DynamicResource TextControlForeground}" |
||||
Effect="{StaticResource TextDropShadowEffect}" |
||||
Text="{Binding Workflow.Creator.Name}" /> |
||||
</StackPanel> |
||||
</Button> |
||||
</Border> |
||||
|
||||
<Border |
||||
Name="ModelCardBottom" |
||||
Grid.Row="1"> |
||||
<TextBlock |
||||
Padding="16" |
||||
Margin="8,0,8,0" |
||||
HorizontalAlignment="Center" |
||||
VerticalAlignment="Center" |
||||
FontWeight="SemiBold" |
||||
Foreground="{DynamicResource TextControlForeground}" |
||||
LetterSpacing="0.33" |
||||
TextWrapping="Wrap" |
||||
MaxWidth="315" |
||||
Text="{Binding Workflow.Name}" |
||||
ToolTip.Tip="{Binding Workflow.Name}" /> |
||||
</Border> |
||||
</Grid> |
||||
</Button> |
||||
</Border> |
||||
</DataTemplate> |
||||
</ItemsRepeater.ItemTemplate> |
||||
</ItemsRepeater> |
||||
</ScrollViewer> |
||||
</Grid> |
||||
</controls:UserControlBase> |
@ -0,0 +1,13 @@
|
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Core.Attributes; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Views; |
||||
|
||||
[Singleton] |
||||
public partial class InstalledWorkflowsPage : UserControlBase |
||||
{ |
||||
public InstalledWorkflowsPage() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
} |
@ -0,0 +1,360 @@
|
||||
<controls:UserControlBase xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" |
||||
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" |
||||
xmlns:openArt="clr-namespace:StabilityMatrix.Core.Models.Api.OpenArt;assembly=StabilityMatrix.Core" |
||||
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" |
||||
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers" |
||||
xmlns:avalonia="https://github.com/projektanker/icons.avalonia" |
||||
xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia" |
||||
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" |
||||
xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" |
||||
x:DataType="viewModels:OpenArtBrowserViewModel" |
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
x:Class="StabilityMatrix.Avalonia.Views.OpenArtBrowserPage"> |
||||
<UserControl.Styles> |
||||
<Style Selector="Border#HoverBorder"> |
||||
<Setter Property="Transitions"> |
||||
<Transitions> |
||||
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.237" /> |
||||
</Transitions> |
||||
</Setter> |
||||
|
||||
<Style Selector="^ labs|AsyncImage"> |
||||
<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="^ labs|AsyncImage"> |
||||
<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="^ labs|AsyncImage"> |
||||
<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> |
||||
<input:StandardUICommand |
||||
x:Key="OpenModelCommand" |
||||
Command="{Binding OpenModelCommand}" /> |
||||
|
||||
<input:StandardUICommand |
||||
x:Key="OpenOnOpenArtCommand" |
||||
Command="{Binding OpenOnOpenArtCommand}" /> |
||||
|
||||
<input:StandardUICommand |
||||
x:Key="OpenWorkflowCommand" |
||||
Command="{Binding OpenWorkflowCommand}" /> |
||||
|
||||
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter" /> |
||||
<DataTemplate x:Key="OpenArtWorkflowTemplate" DataType="{x:Type openArt:OpenArtSearchResult}"> |
||||
<Border |
||||
Name="HoverBorder" |
||||
Padding="0" |
||||
BorderThickness="0" |
||||
Margin="8" |
||||
ClipToBounds="True" |
||||
CornerRadius="8"> |
||||
<Border.ContextFlyout> |
||||
<MenuFlyout> |
||||
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnOpenArt}" |
||||
Command="{StaticResource OpenOnOpenArtCommand}" |
||||
CommandParameter="{Binding }"> |
||||
<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="{StaticResource OpenWorkflowCommand}" |
||||
CommandParameter="{Binding }"> |
||||
<Grid RowDefinitions="*, Auto"> |
||||
<labs:AsyncImage |
||||
Grid.Row="0" |
||||
Grid.RowSpan="2" |
||||
CornerRadius="8" |
||||
Width="330" |
||||
Height="400" |
||||
Source="{Binding Thumbnails[0].Url}" |
||||
Stretch="UniformToFill" /> |
||||
|
||||
<!-- Username pill card --> |
||||
<Border |
||||
BoxShadow="inset 1.2 0 80 1.8 #66000000" |
||||
CornerRadius="16" |
||||
Margin="4" |
||||
Grid.Row="0" |
||||
ClipToBounds="True" |
||||
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 Creator.DevProfileUrl}" |
||||
CornerRadius="16" |
||||
Classes="transparent" |
||||
Padding="10,4"> |
||||
<StackPanel Orientation="Horizontal" Spacing="6"> |
||||
<labs:AsyncImage |
||||
Width="22" |
||||
Height="22" |
||||
ClipToBounds="True" |
||||
Effect="{StaticResource ImageDropShadowEffect}" |
||||
CornerRadius="11" |
||||
RenderOptions.BitmapInterpolationMode="HighQuality" |
||||
IsVisible="{Binding Creator.Avatar, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" |
||||
Source="{Binding Creator.Avatar}"> |
||||
</labs:AsyncImage> |
||||
<TextBlock |
||||
VerticalAlignment="Center" |
||||
Foreground="{DynamicResource TextControlForeground}" |
||||
Effect="{StaticResource TextDropShadowEffect}" |
||||
Text="{Binding Creator.Name}" /> |
||||
</StackPanel> |
||||
</Button> |
||||
</Border> |
||||
|
||||
<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 Name}" |
||||
TextWrapping="NoWrap" |
||||
ToolTip.Tip="{Binding Name}" /> |
||||
|
||||
<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 Stats.Rating}" /> |
||||
<TextBlock |
||||
Margin="4,0,0,0" |
||||
VerticalAlignment="Center" |
||||
Text="{Binding Stats.NumReviews}" |
||||
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 Stats.NumLikes, Converter={StaticResource KiloFormatterConverter}}" /> |
||||
|
||||
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" /> |
||||
<TextBlock |
||||
Margin="0,0,4,0" |
||||
VerticalAlignment="Center" |
||||
Text="{Binding Stats.NumDownloads, 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="{StaticResource OpenModelCommand}" |
||||
CommandParameter="{Binding }" |
||||
Header="{x:Static lang:Resources.Action_OpenOnOpenArt}"> |
||||
<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, *, Auto"> |
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" |
||||
Margin="8"> |
||||
<TextBox |
||||
HorizontalAlignment="Stretch" |
||||
Text="{Binding SearchQuery, Mode=TwoWay}" |
||||
Watermark="{x:Static lang:Resources.Action_Search}" |
||||
Classes="search"/> |
||||
|
||||
<Button |
||||
Grid.Column="1" |
||||
Width="80" |
||||
Margin="8,0,8,0" |
||||
VerticalAlignment="Stretch" |
||||
Classes="accent" |
||||
Command="{Binding SearchButtonCommand}" |
||||
IsDefault="True"> |
||||
<Grid> |
||||
<controls:ProgressRing |
||||
MinWidth="16" |
||||
MinHeight="16" |
||||
VerticalAlignment="Center" |
||||
BorderThickness="4" |
||||
IsIndeterminate="True" |
||||
IsVisible="{Binding SearchButtonCommand.IsRunning}" /> |
||||
<TextBlock |
||||
VerticalAlignment="Center" |
||||
IsVisible="{Binding !SearchButtonCommand.IsRunning}" |
||||
Text="{x:Static lang:Resources.Action_Search}" /> |
||||
</Grid> |
||||
</Button> |
||||
</Grid> |
||||
|
||||
<StackPanel Grid.Row="1" |
||||
Margin="8,0,0,8" |
||||
Orientation="Vertical" |
||||
IsVisible="{Binding SearchQuery, Converter={x:Static StringConverters.IsNullOrEmpty}}"> |
||||
<Label Content="{x:Static lang:Resources.Label_Sort}" /> |
||||
<ComboBox |
||||
MinWidth="100" |
||||
ItemsSource="{Binding AllSortModes}" |
||||
SelectedItem="{Binding SelectedSortMode}"/> |
||||
</StackPanel> |
||||
|
||||
<controls:ProgressRing Grid.Row="2" |
||||
IsVisible="{Binding IsLoading}" |
||||
IsIndeterminate="True" |
||||
Width="128" |
||||
Height="128"/> |
||||
|
||||
<ScrollViewer Grid.Row="2" |
||||
ScrollChanged="ScrollViewer_OnScrollChanged" |
||||
IsVisible="{Binding !IsLoading}"> |
||||
<ItemsRepeater ItemsSource="{Binding SearchResults}" |
||||
ItemTemplate="{StaticResource OpenArtWorkflowTemplate}"> |
||||
<ItemsRepeater.Layout> |
||||
<UniformGridLayout MinColumnSpacing="4" MinRowSpacing="4"/> |
||||
</ItemsRepeater.Layout> |
||||
</ItemsRepeater> |
||||
</ScrollViewer> |
||||
|
||||
<StackPanel Grid.Row="3" |
||||
HorizontalAlignment="Center" |
||||
Margin="0,8,0,8" |
||||
Orientation="Horizontal"> |
||||
<Button |
||||
Margin="0,0,8,0" |
||||
Command="{Binding FirstPageCommand}" |
||||
IsEnabled="{Binding CanGoBack}" |
||||
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 PreviousPageCommand}" |
||||
IsEnabled="{Binding CanGoBack}" |
||||
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 DisplayedPageNumber, FallbackValue=1}" |
||||
VerticalAlignment="Center" |
||||
SpinButtonPlacementMode="Hidden" |
||||
TextAlignment="Center"/> |
||||
<TextBlock Margin="4,0,8,0" VerticalAlignment="Center"> |
||||
<Run Text="/"/> |
||||
<Run Text="{Binding PageCount, FallbackValue=5}"/> |
||||
</TextBlock> |
||||
<Button |
||||
Margin="16,0,8,0" |
||||
Command="{Binding NextPageCommand}" |
||||
IsEnabled="{Binding CanGoForward}" |
||||
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}"> |
||||
<avalonia:Icon Value="fa-solid fa-caret-right" /> |
||||
</Button> |
||||
<Button |
||||
Command="{Binding LastPageCommand}" |
||||
IsEnabled="{Binding CanGoToEnd}" |
||||
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}"> |
||||
<avalonia:Icon Value="fa-solid fa-forward-fast" /> |
||||
</Button> |
||||
</StackPanel> |
||||
</Grid> |
||||
</controls:UserControlBase> |
@ -0,0 +1,41 @@
|
||||
using System; |
||||
using AsyncAwaitBestPractices; |
||||
using Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
using StabilityMatrix.Core.Attributes; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Views; |
||||
|
||||
[Singleton] |
||||
public partial class OpenArtBrowserPage : UserControlBase |
||||
{ |
||||
private readonly ISettingsManager settingsManager; |
||||
|
||||
public OpenArtBrowserPage(ISettingsManager settingsManager) |
||||
{ |
||||
this.settingsManager = settingsManager; |
||||
InitializeComponent(); |
||||
} |
||||
|
||||
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) |
||||
{ |
||||
if (sender is not ScrollViewer scrollViewer) |
||||
return; |
||||
|
||||
if (scrollViewer.Offset.Y == 0) |
||||
return; |
||||
|
||||
var isAtEnd = Math.Abs(scrollViewer.Offset.Y - scrollViewer.ScrollBarMaximum.Y) < 1f; |
||||
|
||||
if ( |
||||
isAtEnd |
||||
&& settingsManager.Settings.IsWorkflowInfiniteScrollEnabled |
||||
&& DataContext is IInfinitelyScroll scroll |
||||
) |
||||
{ |
||||
scroll.LoadNextPageAsync().SafeFireAndForget(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,12 @@
|
||||
<controls:UserControlBase xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" |
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
x:DataType="viewModels:WorkflowsPageViewModel" |
||||
x:Class="StabilityMatrix.Avalonia.Views.WorkflowsPage"> |
||||
<TabControl ItemsSource="{Binding Pages}" |
||||
SelectedItem="{Binding SelectedPage}"/> |
||||
</controls:UserControlBase> |
@ -0,0 +1,13 @@
|
||||
using StabilityMatrix.Avalonia.Controls; |
||||
using StabilityMatrix.Core.Attributes; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Views; |
||||
|
||||
[Singleton] |
||||
public partial class WorkflowsPage : UserControlBase |
||||
{ |
||||
public WorkflowsPage() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
} |
@ -0,0 +1,17 @@
|
||||
using Refit; |
||||
using StabilityMatrix.Core.Models.Api.OpenArt; |
||||
|
||||
namespace StabilityMatrix.Core.Api; |
||||
|
||||
[Headers("User-Agent: StabilityMatrix")] |
||||
public interface IOpenArtApi |
||||
{ |
||||
[Get("/feed")] |
||||
Task<OpenArtSearchResponse> GetFeedAsync([Query] OpenArtFeedRequest request); |
||||
|
||||
[Get("/list")] |
||||
Task<OpenArtSearchResponse> SearchAsync([Query] OpenArtSearchRequest request); |
||||
|
||||
[Post("/download")] |
||||
Task<OpenArtDownloadResponse> DownloadWorkflowAsync([Body] OpenArtDownloadRequest request); |
||||
} |
@ -0,0 +1,124 @@
|
||||
using System.Diagnostics.CodeAnalysis; |
||||
using JetBrains.Annotations; |
||||
|
||||
namespace StabilityMatrix.Core.Models.Api.Comfy; |
||||
|
||||
/// <summary> |
||||
/// Collection of preprocessors included in |
||||
/// </summary> |
||||
/// <param name="Value"></param> |
||||
[PublicAPI] |
||||
[SuppressMessage("ReSharper", "InconsistentNaming")] |
||||
public record ComfyAuxPreprocessor(string Value) : StringValue(Value) |
||||
{ |
||||
public static ComfyAuxPreprocessor None { get; } = new("none"); |
||||
public static ComfyAuxPreprocessor AnimeFaceSemSegPreprocessor { get; } = |
||||
new("AnimeFace_SemSegPreprocessor"); |
||||
public static ComfyAuxPreprocessor BinaryPreprocessor { get; } = new("BinaryPreprocessor"); |
||||
public static ComfyAuxPreprocessor CannyEdgePreprocessor { get; } = new("CannyEdgePreprocessor"); |
||||
public static ComfyAuxPreprocessor ColorPreprocessor { get; } = new("ColorPreprocessor"); |
||||
public static ComfyAuxPreprocessor DensePosePreprocessor { get; } = new("DensePosePreprocessor"); |
||||
public static ComfyAuxPreprocessor DepthAnythingPreprocessor { get; } = new("DepthAnythingPreprocessor"); |
||||
public static ComfyAuxPreprocessor ZoeDepthAnythingPreprocessor { get; } = |
||||
new("Zoe_DepthAnythingPreprocessor"); |
||||
public static ComfyAuxPreprocessor DiffusionEdgePreprocessor { get; } = new("DiffusionEdge_Preprocessor"); |
||||
public static ComfyAuxPreprocessor DWPreprocessor { get; } = new("DWPreprocessor"); |
||||
public static ComfyAuxPreprocessor AnimalPosePreprocessor { get; } = new("AnimalPosePreprocessor"); |
||||
public static ComfyAuxPreprocessor HEDPreprocessor { get; } = new("HEDPreprocessor"); |
||||
public static ComfyAuxPreprocessor FakeScribblePreprocessor { get; } = new("FakeScribblePreprocessor"); |
||||
public static ComfyAuxPreprocessor LeReSDepthMapPreprocessor { get; } = new("LeReS-DepthMapPreprocessor"); |
||||
public static ComfyAuxPreprocessor LineArtPreprocessor { get; } = new("LineArtPreprocessor"); |
||||
public static ComfyAuxPreprocessor AnimeLineArtPreprocessor { get; } = new("AnimeLineArtPreprocessor"); |
||||
public static ComfyAuxPreprocessor LineartStandardPreprocessor { get; } = |
||||
new("LineartStandardPreprocessor"); |
||||
public static ComfyAuxPreprocessor Manga2AnimeLineArtPreprocessor { get; } = |
||||
new("Manga2Anime_LineArt_Preprocessor"); |
||||
public static ComfyAuxPreprocessor MediaPipeFaceMeshPreprocessor { get; } = |
||||
new("MediaPipe-FaceMeshPreprocessor"); |
||||
public static ComfyAuxPreprocessor MeshGraphormerDepthMapPreprocessor { get; } = |
||||
new("MeshGraphormer-DepthMapPreprocessor"); |
||||
public static ComfyAuxPreprocessor MiDaSNormalMapPreprocessor { get; } = |
||||
new("MiDaS-NormalMapPreprocessor"); |
||||
public static ComfyAuxPreprocessor MiDaSDepthMapPreprocessor { get; } = new("MiDaS-DepthMapPreprocessor"); |
||||
public static ComfyAuxPreprocessor MLSDPreprocessor { get; } = new("M-LSDPreprocessor"); |
||||
public static ComfyAuxPreprocessor BAENormalMapPreprocessor { get; } = new("BAE-NormalMapPreprocessor"); |
||||
public static ComfyAuxPreprocessor OneFormerCOCOSemSegPreprocessor { get; } = |
||||
new("OneFormer-COCO-SemSegPreprocessor"); |
||||
public static ComfyAuxPreprocessor OneFormerADE20KSemSegPreprocessor { get; } = |
||||
new("OneFormer-ADE20K-SemSegPreprocessor"); |
||||
public static ComfyAuxPreprocessor OpenposePreprocessor { get; } = new("OpenposePreprocessor"); |
||||
public static ComfyAuxPreprocessor PiDiNetPreprocessor { get; } = new("PiDiNetPreprocessor"); |
||||
public static ComfyAuxPreprocessor SavePoseKpsAsJsonFile { get; } = new("SavePoseKpsAsJsonFile"); |
||||
public static ComfyAuxPreprocessor FacialPartColoringFromPoseKps { get; } = |
||||
new("FacialPartColoringFromPoseKps"); |
||||
public static ComfyAuxPreprocessor ImageLuminanceDetector { get; } = new("ImageLuminanceDetector"); |
||||
public static ComfyAuxPreprocessor ImageIntensityDetector { get; } = new("ImageIntensityDetector"); |
||||
public static ComfyAuxPreprocessor ScribblePreprocessor { get; } = new("ScribblePreprocessor"); |
||||
public static ComfyAuxPreprocessor ScribbleXDoGPreprocessor { get; } = new("Scribble_XDoG_Preprocessor"); |
||||
public static ComfyAuxPreprocessor SAMPreprocessor { get; } = new("SAMPreprocessor"); |
||||
public static ComfyAuxPreprocessor ShufflePreprocessor { get; } = new("ShufflePreprocessor"); |
||||
public static ComfyAuxPreprocessor TEEDPreprocessor { get; } = new("TEEDPreprocessor"); |
||||
public static ComfyAuxPreprocessor TilePreprocessor { get; } = new("TilePreprocessor"); |
||||
public static ComfyAuxPreprocessor UniFormerSemSegPreprocessor { get; } = |
||||
new("UniFormer-SemSegPreprocessor"); |
||||
public static ComfyAuxPreprocessor SemSegPreprocessor { get; } = new("SemSegPreprocessor"); |
||||
public static ComfyAuxPreprocessor UnimatchOptFlowPreprocessor { get; } = |
||||
new("Unimatch_OptFlowPreprocessor"); |
||||
public static ComfyAuxPreprocessor MaskOptFlow { get; } = new("MaskOptFlow"); |
||||
public static ComfyAuxPreprocessor ZoeDepthMapPreprocessor { get; } = new("Zoe-DepthMapPreprocessor"); |
||||
|
||||
private static Dictionary<ComfyAuxPreprocessor, string> DisplayNamesMapping { get; } = |
||||
new() |
||||
{ |
||||
[None] = "None", |
||||
[AnimeFaceSemSegPreprocessor] = "Anime Face SemSeg Preprocessor", |
||||
[BinaryPreprocessor] = "Binary Preprocessor", |
||||
[CannyEdgePreprocessor] = "Canny Edge Preprocessor", |
||||
[ColorPreprocessor] = "Color Preprocessor", |
||||
[DensePosePreprocessor] = "DensePose Preprocessor", |
||||
[DepthAnythingPreprocessor] = "Depth Anything Preprocessor", |
||||
[ZoeDepthAnythingPreprocessor] = "Zoe Depth Anything Preprocessor", |
||||
[DiffusionEdgePreprocessor] = "Diffusion Edge Preprocessor", |
||||
[DWPreprocessor] = "DW Preprocessor", |
||||
[AnimalPosePreprocessor] = "Animal Pose Preprocessor", |
||||
[HEDPreprocessor] = "HED Preprocessor", |
||||
[FakeScribblePreprocessor] = "Fake Scribble Preprocessor", |
||||
[LeReSDepthMapPreprocessor] = "LeReS-DepthMap Preprocessor", |
||||
[LineArtPreprocessor] = "LineArt Preprocessor", |
||||
[AnimeLineArtPreprocessor] = "Anime LineArt Preprocessor", |
||||
[LineartStandardPreprocessor] = "Lineart Standard Preprocessor", |
||||
[Manga2AnimeLineArtPreprocessor] = "Manga2Anime LineArt Preprocessor", |
||||
[MediaPipeFaceMeshPreprocessor] = "MediaPipe FaceMesh Preprocessor", |
||||
[MeshGraphormerDepthMapPreprocessor] = "MeshGraphormer DepthMap Preprocessor", |
||||
[MiDaSNormalMapPreprocessor] = "MiDaS NormalMap Preprocessor", |
||||
[MiDaSDepthMapPreprocessor] = "MiDaS DepthMap Preprocessor", |
||||
[MLSDPreprocessor] = "M-LSD Preprocessor", |
||||
[BAENormalMapPreprocessor] = "BAE NormalMap Preprocessor", |
||||
[OneFormerCOCOSemSegPreprocessor] = "OneFormer COCO SemSeg Preprocessor", |
||||
[OneFormerADE20KSemSegPreprocessor] = "OneFormer ADE20K SemSeg Preprocessor", |
||||
[OpenposePreprocessor] = "Openpose Preprocessor", |
||||
[PiDiNetPreprocessor] = "PiDiNet Preprocessor", |
||||
[SavePoseKpsAsJsonFile] = "Save Pose Kps As Json File", |
||||
[FacialPartColoringFromPoseKps] = "Facial Part Coloring From Pose Kps", |
||||
[ImageLuminanceDetector] = "Image Luminance Detector", |
||||
[ImageIntensityDetector] = "Image Intensity Detector", |
||||
[ScribblePreprocessor] = "Scribble Preprocessor", |
||||
[ScribbleXDoGPreprocessor] = "Scribble XDoG Preprocessor", |
||||
[SAMPreprocessor] = "SAM Preprocessor", |
||||
[ShufflePreprocessor] = "Shuffle Preprocessor", |
||||
[TEEDPreprocessor] = "TEED Preprocessor", |
||||
[TilePreprocessor] = "Tile Preprocessor", |
||||
[UniFormerSemSegPreprocessor] = "UniFormer SemSeg Preprocessor", |
||||
[SemSegPreprocessor] = "SemSeg Preprocessor", |
||||
[UnimatchOptFlowPreprocessor] = "Unimatch OptFlow Preprocessor", |
||||
[MaskOptFlow] = "Mask OptFlow", |
||||
[ZoeDepthMapPreprocessor] = "Zoe DepthMap Preprocessor" |
||||
}; |
||||
|
||||
public static IEnumerable<ComfyAuxPreprocessor> Defaults => DisplayNamesMapping.Keys; |
||||
|
||||
public string DisplayName => DisplayNamesMapping.GetValueOrDefault(this, Value); |
||||
|
||||
/// <inheritdoc /> |
||||
public override string ToString() => Value; |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue