using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Collections; using Avalonia.Controls.Notifications; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Media.Animation; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Settings; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Services; using Notification = DesktopNotifications.Notification; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels.Progress; [View(typeof(ProgressManagerPage))] [ManagedService] [Singleton] public partial class ProgressManagerViewModel : PageViewModelBase { private readonly INotificationService notificationService; private readonly INavigationService navigationService; private readonly INavigationService settingsNavService; public override string Title => "Download Manager"; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.ArrowCircleDown, IsFilled = true }; public AvaloniaList ProgressItems { get; } = new(); [ObservableProperty] private bool isOpen; public ProgressManagerViewModel( ITrackedDownloadService trackedDownloadService, INotificationService notificationService, INavigationService navigationService, INavigationService settingsNavService ) { this.notificationService = notificationService; this.navigationService = navigationService; this.settingsNavService = settingsNavService; // Attach to the event trackedDownloadService.DownloadAdded += TrackedDownloadService_OnDownloadAdded; EventManager.Instance.ToggleProgressFlyout += (_, _) => IsOpen = !IsOpen; EventManager.Instance.PackageInstallProgressAdded += InstanceOnPackageInstallProgressAdded; EventManager.Instance.RecommendedModelsDialogClosed += InstanceOnRecommendedModelsDialogClosed; } private void InstanceOnRecommendedModelsDialogClosed(object? sender, EventArgs e) { var vm = ProgressItems.OfType().FirstOrDefault(); vm?.ShowProgressDialog().SafeFireAndForget(); } private void InstanceOnPackageInstallProgressAdded(object? sender, IPackageModificationRunner runner) { AddPackageInstall(runner).SafeFireAndForget(); } private void TrackedDownloadService_OnDownloadAdded(object? sender, TrackedDownload e) { // Attach notification handlers // Use Changing because Changed might be called after the download is removed e.ProgressStateChanged += (s, state) => { Debug.WriteLine($"Download {e.FileName} state changed to {state}"); var download = s as TrackedDownload; switch (state) { case ProgressState.Success: var imageFile = e.DownloadDirectory.EnumerateFiles( $"{Path.GetFileNameWithoutExtension(e.FileName)}.preview.*" ) .FirstOrDefault(); notificationService .ShowAsync( NotificationKey.Download_Completed, new Notification { Title = "Download Completed", Body = $"Download of {e.FileName} completed successfully.", BodyImagePath = imageFile?.FullPath } ) .SafeFireAndForget(); break; case ProgressState.Failed: var msg = ""; if (download?.Exception is { } exception) { if ( exception is UnauthorizedAccessException || exception.InnerException is UnauthorizedAccessException ) { Dispatcher.UIThread.InvokeAsync(async () => { var errorDialog = new BetterContentDialog { Title = Resources.Label_DownloadFailed, Content = Resources.Label_CivitAiLoginRequired, PrimaryButtonText = "Go to Settings", SecondaryButtonText = "Close", DefaultButton = ContentDialogButton.Primary, }; var result = await errorDialog.ShowAsync(); if (result == ContentDialogResult.Primary) { navigationService.NavigateTo( new SuppressNavigationTransitionInfo() ); await Task.Delay(100); settingsNavService.NavigateTo( new SuppressNavigationTransitionInfo() ); } }); return; } msg = $"({exception.GetType().Name}) {exception.InnerException?.Message ?? exception.Message}"; } notificationService .ShowAsync( NotificationKey.Download_Failed, new Notification { Title = "Download Failed", Body = $"Download of {e.FileName} failed: {msg}" } ) .SafeFireAndForget(); break; case ProgressState.Cancelled: notificationService .ShowAsync( NotificationKey.Download_Canceled, new Notification { Title = "Download Cancelled", Body = $"Download of {e.FileName} was cancelled." } ) .SafeFireAndForget(); break; } }; var vm = new DownloadProgressItemViewModel(e); ProgressItems.Add(vm); } public void AddDownloads(IEnumerable downloads) { foreach (var download in downloads) { if (ProgressItems.Any(vm => vm.Id == download.Id)) { continue; } var vm = new DownloadProgressItemViewModel(download); ProgressItems.Add(vm); } } private Task AddPackageInstall(IPackageModificationRunner packageModificationRunner) { if (ProgressItems.Any(vm => vm.Id == packageModificationRunner.Id)) { return Task.CompletedTask; } var vm = new PackageInstallProgressItemViewModel(packageModificationRunner); ProgressItems.Add(vm); return packageModificationRunner.ShowDialogOnStart ? vm.ShowProgressDialog() : Task.CompletedTask; } private void ShowFailedNotification(string title, string message) { notificationService.ShowPersistent(title, message, NotificationType.Error); } public void StartEventListener() { EventManager.Instance.ProgressChanged += OnProgressChanged; } public void ClearDownloads() { ProgressItems.RemoveAll(ProgressItems.Where(x => x.IsCompleted)); } private void OnProgressChanged(object? sender, ProgressItem e) { if (ProgressItems.Any(x => x.Id == e.ProgressId)) return; ProgressItems.Add(new ProgressItemViewModel(e)); } }