Browse Source

replace SnackbarService with NotificationService

pull/55/head
JT 1 year ago
parent
commit
e53bf91f7d
  1. 10
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  2. 16
      StabilityMatrix.Avalonia/DesignData/MockNotificationService.cs
  3. 32
      StabilityMatrix.Avalonia/Services/INotificationService.cs
  4. 42
      StabilityMatrix.Avalonia/Services/NotificationService.cs
  5. 2
      StabilityMatrix.Avalonia/Styles/ThemeColors.axaml
  6. 77
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserCardViewModel.cs
  7. 26
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  8. 40
      StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
  9. 173
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  10. 6
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
  11. 1
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  12. 36
      StabilityMatrix.Avalonia/Views/LaunchPageView.axaml
  13. 11
      StabilityMatrix.Avalonia/Views/MainWindow.axaml
  14. 2
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs

10
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -49,12 +49,12 @@ public static class DesignData
var modelFinder = new ModelFinder(null!, null!); var modelFinder = new ModelFinder(null!, null!);
LaunchPageViewModel = new LaunchPageViewModel( LaunchPageViewModel = new LaunchPageViewModel(
null!, settingsManager, packageFactory, new PyRunner()); null!, settingsManager, packageFactory, new PyRunner(), notificationService);
LaunchPageViewModel.InstalledPackages.AddRange(settingsManager.Settings.InstalledPackages); LaunchPageViewModel.InstalledPackages.AddRange(settingsManager.Settings.InstalledPackages);
LaunchPageViewModel.SelectedPackage = settingsManager.Settings.InstalledPackages[0]; LaunchPageViewModel.SelectedPackage = settingsManager.Settings.InstalledPackages[0];
PackageManagerViewModel = new PackageManagerViewModel(settingsManager, packageFactory); PackageManagerViewModel = new PackageManagerViewModel(null!, settingsManager, packageFactory, notificationService);
SettingsViewModel = new SettingsViewModel(notificationService); SettingsViewModel = new SettingsViewModel(notificationService);
SelectModelVersionViewModel = new SelectModelVersionViewModel(new CivitModel SelectModelVersionViewModel = new SelectModelVersionViewModel(new CivitModel
@ -112,7 +112,8 @@ public static class DesignData
}; };
CheckpointBrowserViewModel = CheckpointBrowserViewModel =
new CheckpointBrowserViewModel(null!, downloadService, settingsManager, null!, null!) new CheckpointBrowserViewModel(null!, downloadService, settingsManager, null!, null!,
notificationService)
{ {
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel> ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
{ {
@ -120,7 +121,8 @@ public static class DesignData
{ {
Name = "BB95 Furry Mix", Name = "BB95 Furry Mix",
Description = "A furry mix of BB95", Description = "A furry mix of BB95",
}, downloadService, settingsManager, new DialogFactory(settingsManager, downloadService)) }, downloadService, settingsManager,
new DialogFactory(settingsManager, downloadService), notificationService)
} }
}; };

16
StabilityMatrix.Avalonia/DesignData/MockNotificationService.cs

@ -1,6 +1,8 @@
using Avalonia; using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.DesignData; namespace StabilityMatrix.Avalonia.DesignData;
@ -14,4 +16,16 @@ public class MockNotificationService : INotificationService
public void Show(INotification notification) public void Show(INotification notification)
{ {
} }
public Task<TaskResult<T>> TryAsync<T>(Task<T> task, string title = "Error", string? message = null,
NotificationType appearance = NotificationType.Error)
{
return Task.FromResult(new TaskResult<T>(default!));
}
public Task<TaskResult<bool>> TryAsync(Task task, string title = "Error", string? message = null,
NotificationType appearance = NotificationType.Error)
{
return Task.FromResult(new TaskResult<bool>(true));
}
} }

32
StabilityMatrix.Avalonia/Services/INotificationService.cs

@ -1,5 +1,8 @@
using Avalonia; using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Services; namespace StabilityMatrix.Avalonia.Services;
@ -11,4 +14,31 @@ public interface INotificationService
int maxItems = 3); int maxItems = 3);
public void Show(INotification notification); public void Show(INotification notification);
/// <summary>
/// Attempt to run the given task, showing a generic error notification if it fails.
/// </summary>
/// <param name="task">The task to run.</param>
/// <param name="title">The title to show in the notification.</param>
/// <param name="message">The message to show, default to exception.Message</param>
/// <param name="appearance">The appearance of the notification.</param>
Task<TaskResult<T>> TryAsync<T>(
Task<T> task,
string title = "Error",
string? message = null,
NotificationType appearance = NotificationType.Error);
/// <summary>
/// Attempt to run the given void task, showing a generic error notification if it fails.
/// Return a TaskResult with true if the task succeeded, false if it failed.
/// </summary>
/// <param name="task">The task to run.</param>
/// <param name="title">The title to show in the notification.</param>
/// <param name="message">The message to show, default to exception.Message</param>
/// <param name="appearance">The appearance of the notification.</param>
Task<TaskResult<bool>> TryAsync(
Task task,
string title = "Error",
string? message = null,
NotificationType appearance = NotificationType.Error);
} }

42
StabilityMatrix.Avalonia/Services/NotificationService.cs

@ -1,6 +1,9 @@
using Avalonia; using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Services; namespace StabilityMatrix.Avalonia.Services;
@ -25,4 +28,41 @@ public class NotificationService : INotificationService
{ {
notificationManager?.Show(notification); notificationManager?.Show(notification);
} }
/// <inheritdoc />
public async Task<TaskResult<T>> TryAsync<T>(
Task<T> task,
string title = "Error",
string? message = null,
NotificationType appearance = NotificationType.Error)
{
try
{
return new TaskResult<T>(await task);
}
catch (Exception e)
{
Show(new Notification(title, message ?? e.Message, appearance));
return TaskResult<T>.FromException(e);
}
}
/// <inheritdoc />
public async Task<TaskResult<bool>> TryAsync(
Task task,
string title = "Error",
string? message = null,
NotificationType appearance = NotificationType.Error)
{
try
{
await task;
return new TaskResult<bool>(true);
}
catch (Exception e)
{
Show(new Notification(title, message ?? e.Message, appearance));
return new TaskResult<bool>(false, e);
}
}
} }

2
StabilityMatrix.Avalonia/Styles/ThemeColors.axaml

@ -16,7 +16,7 @@
<Color x:Key="ThemeTealColor">#009688</Color> <Color x:Key="ThemeTealColor">#009688</Color>
<Color x:Key="ThemeDarkDarkGreenColor">#2C582C</Color> <Color x:Key="ThemeDarkDarkGreenColor">#2C582C</Color>
<Color x:Key="ThemeDarkGreenColor">#3A783C</Color> <Color x:Key="ThemeDarkGreenColor">#3A783C</Color>
<Color x:Key="ThemeGreenColor">#4CAF50</Color> <Color x:Key="ThemeGreenColor">#4BA04F</Color>
<Color x:Key="ThemeLightGreenColor">#8BC34A</Color> <Color x:Key="ThemeLightGreenColor">#8BC34A</Color>
<Color x:Key="ThemeLimeColor">#CDDC39</Color> <Color x:Key="ThemeLimeColor">#CDDC39</Color>
<Color x:Key="ThemeYellowColor">#FFEB3B</Color> <Color x:Key="ThemeYellowColor">#FFEB3B</Color>

77
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserCardViewModel.cs

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
@ -15,6 +16,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using NLog; using NLog;
using Octokit;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
@ -25,6 +27,7 @@ using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Notification = Avalonia.Controls.Notifications.Notification;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels;
@ -35,6 +38,7 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
private readonly IDownloadService downloadService; private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly IDialogFactory dialogFactory; private readonly IDialogFactory dialogFactory;
private readonly INotificationService notificationService;
public CivitModel CivitModel { get; init; } public CivitModel CivitModel { get; init; }
public Bitmap? CardImage { get; set; } public Bitmap? CardImage { get; set; }
public override bool IsTextVisible => Value > 0; public override bool IsTextVisible => Value > 0;
@ -46,11 +50,13 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
IDownloadService downloadService, IDownloadService downloadService,
ISettingsManager settingsManager, ISettingsManager settingsManager,
IDialogFactory dialogFactory, IDialogFactory dialogFactory,
INotificationService notificationService,
Bitmap? fixedImage = null) Bitmap? fixedImage = null)
{ {
this.downloadService = downloadService; this.downloadService = downloadService;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory; this.dialogFactory = dialogFactory;
this.notificationService = notificationService;
CivitModel = civitModel; CivitModel = civitModel;
if (fixedImage != null) if (fixedImage != null)
@ -78,20 +84,15 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
if (image != null) if (image != null)
{ {
var imageStream = await downloadService.GetImageStreamFromUrl(image.Url); var imageStream = await downloadService.GetImageStreamFromUrl(image.Url);
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() => { CardImage = new Bitmap(imageStream); });
{
CardImage = new Bitmap(imageStream);
});
return; return;
} }
var assetStream = AssetLoader.Open(new Uri("avares://StabilityMatrix.Avalonia/Assets/noimage.png")); var assetStream =
AssetLoader.Open(new Uri("avares://StabilityMatrix.Avalonia/Assets/noimage.png"));
// Otherwise Default image // Otherwise Default image
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() => { CardImage = new Bitmap(assetStream); });
{
CardImage = new Bitmap(assetStream);
});
} }
// On any mode changes, update the image // On any mode changes, update the image
@ -139,7 +140,8 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
await DoImport(model, selectedVersion, selectedFile); await DoImport(model, selectedVersion, selectedFile);
} }
private async Task DoImport(CivitModel model, CivitModelVersion? selectedVersion = null, CivitFile? selectedFile = null) private async Task DoImport(CivitModel model, CivitModelVersion? selectedVersion = null,
CivitFile? selectedFile = null)
{ {
IsImporting = true; IsImporting = true;
Text = "Downloading..."; Text = "Downloading...";
@ -154,9 +156,8 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
var modelVersion = selectedVersion ?? model.ModelVersions?.FirstOrDefault(); var modelVersion = selectedVersion ?? model.ModelVersions?.FirstOrDefault();
if (modelVersion is null) if (modelVersion is null)
{ {
// snackbarService.ShowSnackbarAsync( notificationService.Show(new Notification("Model has no versions available",
// "This model has no versions available for download", "This model has no versions available for download", NotificationType.Warning));
// "Model has no versions available", ControlAppearance.Caution).SafeFireAndForget();
Text = "Unable to Download"; Text = "Unable to Download";
return; return;
} }
@ -166,9 +167,8 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
modelVersion.Files?.FirstOrDefault(x => x.Type == CivitFileType.Model); modelVersion.Files?.FirstOrDefault(x => x.Type == CivitFileType.Model);
if (modelFile is null) if (modelFile is null)
{ {
// snackbarService.ShowSnackbarAsync( notificationService.Show(new Notification("Model has no files available",
// "This model has no files available for download", "This model has no files available for download", NotificationType.Warning));
// "Model has no files available", ControlAppearance.Caution).SafeFireAndForget();
Text = "Unable to Download"; Text = "Unable to Download";
return; return;
} }
@ -192,24 +192,23 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
}); });
})); }));
await downloadTask; var downloadResult =
await notificationService.TryAsync(downloadTask, "Could not download file");
// var downloadResult = await snackbarService.TryAsync(downloadTask, "Could not download file");
// Failed download handling // Failed download handling
// if (downloadResult.Exception is not null) if (downloadResult.Exception is not null)
// { {
// // For exceptions other than ApiException or TaskCanceledException, log error // For exceptions other than ApiException or TaskCanceledException, log error
// var logLevel = downloadResult.Exception switch var logLevel = downloadResult.Exception switch
// { {
// HttpRequestException or ApiException or TaskCanceledException => LogLevel.Warn, HttpRequestException or ApiException or TaskCanceledException => LogLevel.Warn,
// _ => LogLevel.Error _ => LogLevel.Error
// }; };
// Logger.Log(logLevel, downloadResult.Exception, "Error during model download"); Logger.Log(logLevel, downloadResult.Exception, "Error during model download");
//
// Text = "Download Failed"; Text = "Download Failed";
// return; return;
//} }
// When sha256 is available, validate the downloaded file // When sha256 is available, validate the downloaded file
var fileExpectedSha256 = modelFile.Hashes.SHA256; var fileExpectedSha256 = modelFile.Hashes.SHA256;
@ -225,14 +224,15 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
{ {
Text = "Import Failed!"; Text = "Import Failed!";
DelayedClearProgress(TimeSpan.FromMilliseconds(800)); DelayedClearProgress(TimeSpan.FromMilliseconds(800));
// snackbarService.ShowSnackbarAsync( notificationService.Show(new Notification("Download failed hash validation",
// "This may be caused by network or server issues from CivitAI, please try again in a few minutes.", "This may be caused by network or server issues from CivitAI, please try again in a few minutes.",
// "Download failed hash validation").SafeFireAndForget(); NotificationType.Error));
Text = "Download Failed"; Text = "Download Failed";
return; return;
} }
// snackbarService.ShowSnackbarAsync($"{model.Type} {model.Name} imported successfully!",
// "Import complete", ControlAppearance.Info).SafeFireAndForget(); notificationService.Show(new Notification("Import complete",
$"{model.Type} {model.Name} imported successfully!", NotificationType.Success));
} }
IsIndeterminate = true; IsIndeterminate = true;
@ -258,7 +258,7 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
filesForCleanup.Add(imageDownloadPath); filesForCleanup.Add(imageDownloadPath);
var imageTask = var imageTask =
downloadService.DownloadToFileAsync(image.Url, imageDownloadPath); downloadService.DownloadToFileAsync(image.Url, imageDownloadPath);
// await snackbarService.TryAsync(imageTask, "Could not download preview image"); await notificationService.TryAsync(imageTask, "Could not download preview image");
} }
} }
@ -278,6 +278,7 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
file.Delete(); file.Delete();
Logger.Info($"Download cleanup: Deleted file {file}"); Logger.Info($"Download cleanup: Deleted file {file}");
} }
IsIndeterminate = false; IsIndeterminate = false;
Value = 100; Value = 100;
DelayedClearProgress(TimeSpan.FromMilliseconds(800)); DelayedClearProgress(TimeSpan.FromMilliseconds(800));

26
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
@ -37,6 +38,7 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly IDialogFactory dialogFactory; private readonly IDialogFactory dialogFactory;
private readonly ILiteDbContext liteDbContext; private readonly ILiteDbContext liteDbContext;
private readonly INotificationService notificationService;
private const int MaxModelsPerPage = 14; private const int MaxModelsPerPage = 14;
[ObservableProperty] private ObservableCollection<CheckpointBrowserCardViewModel>? modelCards; [ObservableProperty] private ObservableCollection<CheckpointBrowserCardViewModel>? modelCards;
@ -72,13 +74,15 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
IDownloadService downloadService, IDownloadService downloadService,
ISettingsManager settingsManager, ISettingsManager settingsManager,
IDialogFactory dialogFactory, IDialogFactory dialogFactory,
ILiteDbContext liteDbContext) ILiteDbContext liteDbContext,
INotificationService notificationService)
{ {
this.civitApi = civitApi; this.civitApi = civitApi;
this.downloadService = downloadService; this.downloadService = downloadService;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory; this.dialogFactory = dialogFactory;
this.liteDbContext = liteDbContext; this.liteDbContext = liteDbContext;
this.notificationService = notificationService;
CurrentPageNumber = 1; CurrentPageNumber = 1;
CanGoToNextPage = true; CanGoToNextPage = true;
@ -167,26 +171,26 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// snackbarService.ShowSnackbarAsync("Request to CivitAI timed out", notificationService.Show(new Notification("Request to CivitAI timed out",
// "Please try again in a few minutes").SafeFireAndForget(); "Please try again in a few minutes"));
Logger.Warn($"CivitAI query timed out ({request})"); Logger.Warn($"CivitAI query timed out ({request})");
} }
catch (HttpRequestException e) catch (HttpRequestException e)
{ {
// snackbarService.ShowSnackbarAsync("CivitAI can't be reached right now", notificationService.Show(new Notification("CivitAI can't be reached right now",
// "Please try again in a few minutes").SafeFireAndForget(); "Please try again in a few minutes"));
Logger.Warn(e, $"CivitAI query HttpRequestException ({request})"); Logger.Warn(e, $"CivitAI query HttpRequestException ({request})");
} }
catch (ApiException e) catch (ApiException e)
{ {
// snackbarService.ShowSnackbarAsync("CivitAI can't be reached right now", notificationService.Show(new Notification("CivitAI can't be reached right now",
// "Please try again in a few minutes").SafeFireAndForget(); "Please try again in a few minutes"));
Logger.Warn(e, $"CivitAI query ApiException ({request})"); Logger.Warn(e, $"CivitAI query ApiException ({request})");
} }
catch (Exception e) catch (Exception e)
{ {
// snackbarService.ShowSnackbarAsync($"Please try again in a few minutes", notificationService.Show(new Notification("CivitAI can't be reached right now",
// $"Unknown exception during CivitAI query: {e.GetType().Name}").SafeFireAndForget(); $"Unknown exception during CivitAI query: {e.GetType().Name}"));
Logger.Error(e, $"CivitAI query unknown exception ({request})"); Logger.Error(e, $"CivitAI query unknown exception ({request})");
} }
finally finally
@ -209,7 +213,7 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
{ {
var updateCards = models var updateCards = models
.Select(model => new CheckpointBrowserCardViewModel(model, .Select(model => new CheckpointBrowserCardViewModel(model,
downloadService, settingsManager, dialogFactory)).ToList(); downloadService, settingsManager, dialogFactory, notificationService)).ToList();
allModelCards = updateCards; allModelCards = updateCards;
ModelCards = ModelCards =
new ObservableCollection<CheckpointBrowserCardViewModel>( new ObservableCollection<CheckpointBrowserCardViewModel>(
@ -337,7 +341,7 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
// ModelCardsView?.Refresh(); // ModelCardsView?.Refresh();
var updateCards = allModelCards var updateCards = allModelCards
.Select(model => new CheckpointBrowserCardViewModel(model.CivitModel, .Select(model => new CheckpointBrowserCardViewModel(model.CivitModel,
downloadService, settingsManager, dialogFactory)) downloadService, settingsManager, dialogFactory, notificationService))
.Where(FilterModelCardsPredicate); .Where(FilterModelCardsPredicate);
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards); ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards);

40
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

@ -4,12 +4,14 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using Avalonia.Controls.Notifications;
using Avalonia.Threading; using Avalonia.Threading;
using AvaloniaEdit.Document; using AvaloniaEdit.Document;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
@ -28,6 +30,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory; private readonly IPackageFactory packageFactory;
private readonly IPyRunner pyRunner; private readonly IPyRunner pyRunner;
private readonly INotificationService notificationService;
public override string Title => "Launch"; public override string Title => "Launch";
public override Symbol Icon => Symbol.PlayFilled; public override Symbol Icon => Symbol.PlayFilled;
@ -48,12 +51,13 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable
private string webUiUrl = string.Empty; private string webUiUrl = string.Empty;
public LaunchPageViewModel(ILogger<LaunchPageViewModel> logger, ISettingsManager settingsManager, IPackageFactory packageFactory, public LaunchPageViewModel(ILogger<LaunchPageViewModel> logger, ISettingsManager settingsManager, IPackageFactory packageFactory,
IPyRunner pyRunner) IPyRunner pyRunner, INotificationService notificationService)
{ {
this.logger = logger; this.logger = logger;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.packageFactory = packageFactory; this.packageFactory = packageFactory;
this.pyRunner = pyRunner; this.pyRunner = pyRunner;
this.notificationService = notificationService;
} }
public override void OnLoaded() public override void OnLoaded()
@ -84,10 +88,11 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable
if (activeInstall == null) if (activeInstall == null)
{ {
// No selected package: error snackbar // No selected package: error notification
// snackbarService.ShowSnackbarAsync( notificationService.Show(new Notification(
// "You must install and select a package before launching", message: "You must install and select a package before launching",
// "No package selected").SafeFireAndForget(); title: "No package selected",
type: NotificationType.Error));
return; return;
} }
@ -101,9 +106,10 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable
logger.LogWarning( logger.LogWarning(
"During launch, package name '{PackageName}' did not match a definition", "During launch, package name '{PackageName}' did not match a definition",
activeInstallName); activeInstallName);
// snackbarService.ShowSnackbarAsync(
// "Install package name did not match a definition. Please reinstall and let us know about this issue.", notificationService.Show(new Notification("Package name invalid",
// "Package name invalid").SafeFireAndForget(); "Install package name did not match a definition. Please reinstall and let us know about this issue.",
NotificationType.Error));
return; return;
} }
@ -129,7 +135,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable
basePackage.ConsoleOutput += OnProcessOutputReceived; basePackage.ConsoleOutput += OnProcessOutputReceived;
basePackage.Exited += OnProcessExited; basePackage.Exited += OnProcessExited;
// basePackage.StartupComplete += RunningPackageOnStartupComplete; basePackage.StartupComplete += RunningPackageOnStartupComplete;
// Update shared folder links (in case library paths changed) // Update shared folder links (in case library paths changed)
//sharedFolders.UpdateLinksForPackage(basePackage, packagePath); //sharedFolders.UpdateLinksForPackage(basePackage, packagePath);
@ -149,18 +155,26 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable
if (RunningPackage is null) return; if (RunningPackage is null) return;
await RunningPackage.Shutdown(); await RunningPackage.Shutdown();
// runningPackage.StartupComplete -= RunningPackageOnStartupComplete;
RunningPackage = null; RunningPackage = null;
ConsoleDocument.Text += $"{Environment.NewLine}Stopped process at {DateTimeOffset.Now}{Environment.NewLine}"; ConsoleDocument.Text += $"{Environment.NewLine}Stopped process at {DateTimeOffset.Now}{Environment.NewLine}";
ShowWebUiButton = false; ShowWebUiButton = false;
} }
public void OpenWebUi()
{
if (!string.IsNullOrWhiteSpace(webUiUrl))
{
ProcessRunner.OpenUrl(webUiUrl);
}
}
private void OnProcessExited(object? sender, int exitCode) private void OnProcessExited(object? sender, int exitCode)
{ {
if (sender is BasePackage package) if (sender is BasePackage package)
{ {
package.ConsoleOutput -= OnProcessOutputReceived; package.ConsoleOutput -= OnProcessOutputReceived;
package.Exited -= OnProcessExited; package.Exited -= OnProcessExited;
package.StartupComplete -= RunningPackageOnStartupComplete;
} }
RunningPackage = null; RunningPackage = null;
ShowWebUiButton = false; ShowWebUiButton = false;
@ -197,6 +211,12 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable
}); });
} }
private void RunningPackageOnStartupComplete(object? sender, string e)
{
webUiUrl = e;
ShowWebUiButton = !string.IsNullOrWhiteSpace(webUiUrl);
}
private void LoadPackages() private void LoadPackages()
{ {
var packages = settingsManager.Settings.InstalledPackages; var packages = settingsManager.Settings.InstalledPackages;

173
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

@ -1,14 +1,21 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using Polly;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels;
@ -20,22 +27,29 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(PackageManagerPage))] [View(typeof(PackageManagerPage))]
public partial class PackageManagerViewModel : PageViewModelBase public partial class PackageManagerViewModel : PageViewModelBase
{ {
private readonly ILogger<PackageManagerViewModel> logger;
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory; private readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private const int MinutesToWaitForUpdateCheck = 60; private const int MinutesToWaitForUpdateCheck = 60;
public PackageManagerViewModel(ISettingsManager settingsManager, IPackageFactory packageFactory) public PackageManagerViewModel(ILogger<PackageManagerViewModel> logger,
ISettingsManager settingsManager, IPackageFactory packageFactory,
INotificationService notificationService)
{ {
this.logger = logger;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.packageFactory = packageFactory; this.packageFactory = packageFactory;
this.notificationService = notificationService;
ProgressText = string.Empty; ProgressText = string.Empty;
InstallButtonText = "Install"; InstallButtonText = "Launch";
InstallButtonEnabled = true; InstallButtonEnabled = true;
ProgressValue = 0; ProgressValue = 0;
IsIndeterminate = false; IsIndeterminate = false;
Packages = new ObservableCollection<InstalledPackage>(settingsManager.Settings.InstalledPackages); Packages =
new ObservableCollection<InstalledPackage>(settingsManager.Settings.InstalledPackages);
if (Packages.Any()) if (Packages.Any())
{ {
@ -144,7 +158,160 @@ public partial class PackageManagerViewModel : PageViewModelBase
[RelayCommand] [RelayCommand]
private async Task Uninstall() private async Task Uninstall()
{ {
if (SelectedPackage?.LibraryPath == null)
{
logger.LogError("No package selected to uninstall");
return;
}
var dialog = new ContentDialog
{
Title = "Are you sure?",
Content = "This will delete all folders in the package directory, including any generated images in that directory as well as any files you may have added.",
PrimaryButtonText = "Yes, delete it",
CloseButtonText = "No, keep it",
DefaultButton = ContentDialogButton.Primary
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
IsUninstalling = true;
InstallButtonEnabled = false;
var deleteTask = DeleteDirectoryAsync(Path.Combine(settingsManager.LibraryDir,
SelectedPackage.LibraryPath));
var taskResult = await notificationService.TryAsync(deleteTask,
"Some files could not be deleted. Please close any open files in the package directory and try again.");
if (taskResult.IsSuccessful)
{
notificationService.Show(new Notification("Success",
$"Package {SelectedPackage.DisplayName} uninstalled",
NotificationType.Success));
settingsManager.Transaction(settings =>
{
settings.RemoveInstalledPackageAndUpdateActive(SelectedPackage);
});
}
await OnLoadedAsync();
IsUninstalling = false;
InstallButtonEnabled = true;
}
}
/// <summary>
/// Deletes a directory and all of its contents recursively.
/// Uses Polly to retry the deletion if it fails, up to 5 times with an exponential backoff.
/// </summary>
/// <param name="targetDirectory"></param>
private Task DeleteDirectoryAsync(string targetDirectory)
{
var policy = Policy.Handle<IOException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)),
onRetry: (exception, calculatedWaitDuration) =>
{
logger.LogWarning(
exception,
"Deletion of {TargetDirectory} failed. Retrying in {CalculatedWaitDuration}",
targetDirectory, calculatedWaitDuration);
});
return policy.ExecuteAsync(async () =>
{
await Task.Run(() =>
{
DeleteDirectory(targetDirectory);
});
});
}
private void DeleteDirectory(string targetDirectory)
{
// Skip if directory does not exist
if (!Directory.Exists(targetDirectory))
{
return;
}
// For junction points, delete with recursive false
if (new DirectoryInfo(targetDirectory).LinkTarget != null)
{
logger.LogInformation("Removing junction point {TargetDirectory}", targetDirectory);
try
{
Directory.Delete(targetDirectory, false);
return;
}
catch (IOException ex)
{
throw new IOException($"Failed to delete junction point {targetDirectory}", ex);
}
}
// Recursively delete all subdirectories
var subdirectoryEntries = Directory.GetDirectories(targetDirectory);
foreach (var subdirectoryPath in subdirectoryEntries)
{
DeleteDirectory(subdirectoryPath);
}
// Delete all files in the directory
var fileEntries = Directory.GetFiles(targetDirectory);
foreach (var filePath in fileEntries)
{
try
{
File.SetAttributes(filePath, FileAttributes.Normal);
File.Delete(filePath);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete file {filePath}", ex);
}
}
// Delete the target directory itself
try
{
Directory.Delete(targetDirectory, false);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete directory {targetDirectory}", ex);
}
}
private async Task UpdateSelectedPackage()
{
var package = packageFactory.FindPackageByName(SelectedPackage?.PackageName ?? string.Empty);
if (package == null)
{
logger.LogError($"Could not find package {SelectedPackage.PackageName}");
return;
}
ProgressText = $"Updating {SelectedPackage.DisplayName} to latest version...";
package.InstallLocation = SelectedPackage.FullPath!;
var progress = new Progress<ProgressReport>(progress =>
{
var percent = Convert.ToInt32(progress.Percentage);
if (progress.IsIndeterminate || progress.Progress == -1)
{
IsIndeterminate = true;
}
else
{
IsIndeterminate = false;
ProgressValue = percent;
}
ProgressText = $"Updating {SelectedPackage.DisplayName} to latest version... {percent:N0}%";
EventManager.Instance.OnGlobalProgressChanged(percent);
});
var updateResult = await package.Update(SelectedPackage, progress);
ProgressText = "Update complete";
SelectedPackage.UpdateAvailable = false;
UpdateAvailable = false;
settingsManager.UpdatePackageVersionNumber(SelectedPackage.Id, updateResult);
await OnLoadedAsync();
} }
[RelayCommand] [RelayCommand]

6
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml

@ -11,7 +11,11 @@
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}" d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}"
x:CompileBindings="True" x:CompileBindings="True"
x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage"> x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage">
<UserControl.Styles>
<Style Selector="controls|ContentDialog > Border > Panel > controls|FABorder > Border > Grid > Border">
<Setter Property="IsVisible" Value="False"/>
</Style>
</UserControl.Styles>
<UserControl.Resources> <UserControl.Resources>
<DataTemplate DataType="{x:Type viewModels:CheckpointBrowserCardViewModel}" x:Key="CivitModelTemplate"> <DataTemplate DataType="{x:Type viewModels:CheckpointBrowserCardViewModel}" x:Key="CivitModelTemplate">
<controls1:Card <controls1:Card

1
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -1,7 +1,6 @@
<controls:UserControlBase xmlns="https://github.com/avaloniaui" <controls:UserControlBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"

36
StabilityMatrix.Avalonia/Views/LaunchPageView.axaml

@ -18,35 +18,14 @@
mc:Ignorable="d"> mc:Ignorable="d">
<Grid RowDefinitions="Auto,*,Auto"> <Grid RowDefinitions="Auto,*,Auto">
<Grid ColumnDefinitions="Auto,*"> <Grid ColumnDefinitions="Auto,*"
Margin="0,8,0, 8">
<Grid ColumnDefinitions="0.8*,0.2*"> <Grid ColumnDefinitions="0.8*,0.2*">
<!--<ui:Flyout
Background="{DynamicResource SystemAccentColorPrimaryBrush}"
FontSize="18"
Grid.Column="0"
Grid.Row="0"
IsOpen="{Binding IsLaunchTeachingTipsOpen, Mode=TwoWay}"
Margin="24,8,0,0"
Placement="Bottom">
<StackPanel Orientation="Horizontal">
<Grid HorizontalAlignment="Left">
<ui:SymbolIcon Symbol="ArrowCurveUpLeft20" />
</Grid>
<TextBlock
HorizontalAlignment="Left"
Text="Click Launch to get started!"
TextWrapping="WrapWithOverflow"
Width="280" />
</StackPanel>
</ui:Flyout>-->
<!-- Command="{Binding LaunchCommand}" -->
<!-- Visibility="{Binding LaunchButtonVisibility, FallbackValue=Visible}" -->
<Button <Button
Grid.Row="0" Grid.Row="0"
Grid.Column="0" Grid.Column="0"
Width="72" Width="72"
Margin="24,8,0,0" Margin="16,8,0,0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Classes="success" Classes="success"
@ -56,14 +35,13 @@
Grid.Row="0" Grid.Row="0"
Grid.Column="0" Grid.Column="0"
Width="72" Width="72"
Margin="24,8,0,0" Margin="16,8,0,0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Classes="danger" Classes="danger"
Command="{Binding Stop}" Command="{Binding Stop}"
Content="Stop" Content="Stop"
IsVisible="{Binding RunningPackage, Converter={x:Static ObjectConverters.IsNotNull}}" /> IsVisible="{Binding RunningPackage, Converter={x:Static ObjectConverters.IsNotNull}}" />
<!-- Command="{Binding ConfigCommand}" -->
<Button <Button
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
@ -80,7 +58,7 @@
x:Name="SelectPackageComboBox" x:Name="SelectPackageComboBox"
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
Margin="8,8,24,0" Margin="8,8,16,0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Top" VerticalAlignment="Top"
ItemsSource="{Binding InstalledPackages}" ItemsSource="{Binding InstalledPackages}"
@ -108,7 +86,7 @@
<avaloniaEdit:TextEditor <avaloniaEdit:TextEditor
x:Name="Console" x:Name="Console"
Grid.Row="1" Grid.Row="1"
Margin="16,8,16,10" Margin="8,8,16,10"
Document="{Binding ConsoleDocument}" Document="{Binding ConsoleDocument}"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace" FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
IsReadOnly="True" IsReadOnly="True"
@ -126,6 +104,8 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Classes="success" Classes="success"
Content="Open Web UI" Content="Open Web UI"
Command="{Binding OpenWebUi}"
IsVisible="{Binding ShowWebUiButton}"
FontSize="12" /> FontSize="12" />
</Grid> </Grid>

11
StabilityMatrix.Avalonia/Views/MainWindow.axaml

@ -6,8 +6,6 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:avalonia="clr-namespace:StabilityMatrix.Avalonia"
xmlns:controls1="clr-namespace:FAControlsGallery.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
d:DataContext="{x:Static mocks:DesignData.MainWindowViewModel}" d:DataContext="{x:Static mocks:DesignData.MainWindowViewModel}"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
@ -17,11 +15,6 @@
Height="750" Height="750"
Title="Stability Matrix Avalonia" Title="Stability Matrix Avalonia"
x:Class="StabilityMatrix.Avalonia.Views.MainWindow"> x:Class="StabilityMatrix.Avalonia.Views.MainWindow">
<controls:AppWindowBase.Styles>
<Style Selector="ui|ContentDialog > Border > Panel > ui|FABorder > Border > Grid > Border">
<Setter Property="IsVisible" Value="False"/>
</Style>
</controls:AppWindowBase.Styles>
<Grid RowDefinitions="Auto,Auto,*"> <Grid RowDefinitions="Auto,Auto,*">
<Grid Name="TitleBarHost" <Grid Name="TitleBarHost"
@ -64,6 +57,7 @@
IsPaneOpen="False" IsPaneOpen="False"
OpenPaneLength="200" OpenPaneLength="200"
IsSettingsVisible="False" IsSettingsVisible="False"
Name="NavView"
Content="{Binding CurrentPage}" Content="{Binding CurrentPage}"
MenuItemsSource="{Binding Pages, Mode=OneWay}" MenuItemsSource="{Binding Pages, Mode=OneWay}"
FooterMenuItemsSource="{Binding FooterPages, Mode=OneWay}" FooterMenuItemsSource="{Binding FooterPages, Mode=OneWay}"
@ -77,9 +71,6 @@
IconSource="{Binding Icon}"/> IconSource="{Binding Icon}"/>
</DataTemplate> </DataTemplate>
</ui:NavigationView.MenuItemTemplate> </ui:NavigationView.MenuItemTemplate>
<ui:NavigationView.DataTemplates>
<!-- Define your DataTemplates here, could also use ContentTemplate if only 1 template is needed -->
</ui:NavigationView.DataTemplates>
</ui:NavigationView> </ui:NavigationView>
</Grid> </Grid>
</controls:AppWindowBase> </controls:AppWindowBase>

2
StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs

@ -22,7 +22,7 @@ public partial class MainWindow : AppWindowBase
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex; TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
} }
public override async void OnLoaded(object? sender, RoutedEventArgs e) public override void OnLoaded(object? sender, RoutedEventArgs e)
{ {
base.OnLoaded(sender, e); base.OnLoaded(sender, e);
NotificationService?.Initialize(this); NotificationService?.Initialize(this);

Loading…
Cancel
Save