diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 613802e6..9974fe0a 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -129,7 +129,7 @@ public static class DesignData { Title = "Lora", DirectoryPath = "Packages/lora", - CheckpointFiles = new ObservableCollection + CheckpointFiles = new AdvancedObservableList { new() { @@ -162,7 +162,7 @@ public static class DesignData { Title = "VAE", DirectoryPath = "Packages/VAE", - CheckpointFiles = new ObservableCollection + CheckpointFiles = new AdvancedObservableList { new() { diff --git a/StabilityMatrix.Avalonia/DialogHelper.cs b/StabilityMatrix.Avalonia/DialogHelper.cs index 32d071cc..ebeb1aa7 100644 --- a/StabilityMatrix.Avalonia/DialogHelper.cs +++ b/StabilityMatrix.Avalonia/DialogHelper.cs @@ -4,8 +4,10 @@ using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; +using Avalonia; using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Media; using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; @@ -132,6 +134,41 @@ public static class DialogHelper IsPrimaryButtonEnabled = false, }; } + + /// + /// Create a simple title and description task dialog. + /// Sets the XamlRoot to the current top level window. + /// + public static TaskDialog CreateTaskDialog(string title, string description) + { + Dispatcher.UIThread.VerifyAccess(); + + var content = new StackPanel + { + Children = + { + new TextBlock + { + Margin = new Thickness(0,0,0,8), + FontSize = 16, + Text = title, + TextWrapping = TextWrapping.WrapWithOverflow, + }, + new TextBlock + { + Text = description, + TextWrapping = TextWrapping.WrapWithOverflow, + } + } + }; + + return new TaskDialog + { + Title = title, + Content = content, + XamlRoot = App.VisualRoot + }; + } } // Text fields diff --git a/StabilityMatrix.Avalonia/Models/AdvancedObservableList.cs b/StabilityMatrix.Avalonia/Models/AdvancedObservableList.cs new file mode 100644 index 00000000..a9e837a7 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/AdvancedObservableList.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Collections; +using Avalonia.Threading; + +namespace StabilityMatrix.Avalonia.Models; + +/// +/// Observable AvaloniaList supporting child item deletion requests. +/// +public class AdvancedObservableList : AvaloniaList +{ + /// + public AdvancedObservableList() + { + CollectionChanged += CollectionChangedEventRegistrationHandler; + } + + /// + public AdvancedObservableList(IEnumerable items) : base(items) + { + CollectionChanged += CollectionChangedEventRegistrationHandler; + } + + private void CollectionChangedEventRegistrationHandler(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.OldItems != null) + { + foreach (var item in e.OldItems) + { + TryUnregisterRemovableListItem((T)item); + } + } + if (e.NewItems != null) + { + foreach (var item in e.NewItems) + { + TryRegisterRemovableListItem((T)item); + } + } + } + + private void OnItemRemoveRequested(object? sender, EventArgs e) + { + if (sender is T item) + { + Dispatcher.UIThread.Post(() => Remove(item)); + } + } + + private bool TryRegisterRemovableListItem(T item) + { + if (item is IRemovableListItem removableListItem) + { + removableListItem.ParentListRemoveRequested += OnItemRemoveRequested; + return true; + } + return false; + } + + private bool TryUnregisterRemovableListItem(T item) + { + if (item is IRemovableListItem removableListItem) + { + removableListItem.ParentListRemoveRequested -= OnItemRemoveRequested; + return true; + } + return false; + } +} diff --git a/StabilityMatrix.Avalonia/Models/IRemovableListItem.cs b/StabilityMatrix.Avalonia/Models/IRemovableListItem.cs new file mode 100644 index 00000000..1e5dddff --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/IRemovableListItem.cs @@ -0,0 +1,8 @@ +using System; + +namespace StabilityMatrix.Avalonia.Models; + +public interface IRemovableListItem +{ + public event EventHandler ParentListRemoveRequested; +} diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointFile.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointFile.cs index 0bcb7d0e..762bcdaf 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointFile.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointFile.cs @@ -5,10 +5,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; -using AsyncAwaitBestPractices; using Avalonia.Data; -using Avalonia.Media.Imaging; -using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; @@ -24,9 +21,6 @@ namespace StabilityMatrix.Avalonia.ViewModels; public partial class CheckpointFile : ViewModelBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - - // Event for when this file is deleted - public event EventHandler? Deleted; /// /// Absolute path to the checkpoint file. @@ -122,7 +116,7 @@ public partial class CheckpointFile : ViewModelBase IsLoading = false; } } - Deleted?.Invoke(this, this); + RemoveFromParentList(); } [RelayCommand] diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointFolder.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointFolder.cs index b9b608c8..b29b4b64 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointFolder.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointFolder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Collections.Specialized; using System.IO; using System.Linq; @@ -9,10 +8,10 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Platform.Storage; using Avalonia.Threading; -using AvaloniaEdit.Utils; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; +using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; @@ -77,9 +76,9 @@ public partial class CheckpointFolder : ViewModelBase public ProgressViewModel Progress { get; } = new(); public CheckpointFolder? ParentFolder { get; init; } - public ObservableCollection SubFolders { get; init; } = new(); - public ObservableCollection CheckpointFiles { get; init; } = new(); - public ObservableCollection DisplayedCheckpointFiles { get; set; } + public AdvancedObservableList SubFolders { get; init; } = new(); + public AdvancedObservableList CheckpointFiles { get; init; } = new(); + public AdvancedObservableList DisplayedCheckpointFiles { get; set; } public CheckpointFolder( ISettingsManager settingsManager, @@ -121,7 +120,7 @@ public partial class CheckpointFolder : ViewModelBase { var filteredFiles = CheckpointFiles.Where(y => y.FileName.Contains(value, StringComparison.OrdinalIgnoreCase)); - DisplayedCheckpointFiles = new ObservableCollection(filteredFiles); + DisplayedCheckpointFiles = new AdvancedObservableList(filteredFiles); } } @@ -136,31 +135,10 @@ public partial class CheckpointFolder : ViewModelBase settingsManager.SetSharedFolderCategoryVisible(FolderType, value); } } - - // On collection changes + private void OnCheckpointFilesChanged(object? sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(nameof(TitleWithFilesCount)); - if (e.NewItems == null) return; - // On new added items, add event handler for deletion - foreach (CheckpointFile item in e.NewItems) - { - item.Deleted += OnCheckpointFileDelete; - } - } - - /// - /// Handler for CheckpointFile requesting to be deleted from the collection. - /// - /// - /// - private void OnCheckpointFileDelete(object? sender, CheckpointFile file) - { - Dispatcher.UIThread.Post(() => - { - CheckpointFiles.Remove(file); - DisplayedCheckpointFiles.Remove(file); - }); } public async Task OnDrop(DragEventArgs e) @@ -190,6 +168,52 @@ public partial class CheckpointFolder : ViewModelBase await ProcessRunner.OpenFolderBrowser(path); } + [RelayCommand] + private async Task Delete() + { + var directory = new DirectoryPath(DirectoryPath); + + if (!directory.Exists) + { + RemoveFromParentList(); + return; + } + + var dialog = DialogHelper.CreateTaskDialog( + "Are you sure you want to delete this folder?",directory); + + dialog.ShowProgressBar = false; + dialog.Buttons = new List + { + TaskDialogButton.YesButton, + TaskDialogButton.NoButton + }; + + dialog.Closing += async (sender, e) => + { + // We only want to use the deferral on the 'Yes' Button + if ((TaskDialogStandardResult)e.Result == TaskDialogStandardResult.Yes) + { + var deferral = e.GetDeferral(); + + sender.ShowProgressBar = true; + sender.SetProgressBarState(0, TaskDialogProgressState.Indeterminate); + + await using (new MinimumDelay(200, 300)) + { + await directory.DeleteAsync(true); + } + + RemoveFromParentList(); + deferral.Complete(); + } + }; + + dialog.XamlRoot = App.VisualRoot; + + await dialog.ShowAsync(true); + } + [RelayCommand] private async Task CreateSubFolder() { diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs index 76c86fbd..029cde49 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs @@ -36,7 +36,9 @@ public partial class CheckpointsPageViewModel : PageViewModelBase [ObservableProperty] private bool isImportAsConnected; [ObservableProperty] private bool isLoading; [ObservableProperty] private bool isIndexing; - [ObservableProperty] private string searchFilter; + + [ObservableProperty] + private string searchFilter = string.Empty; partial void OnIsImportAsConnectedChanged(bool value) { @@ -47,7 +49,8 @@ public partial class CheckpointsPageViewModel : PageViewModelBase } } - [ObservableProperty] private ObservableCollection checkpointFolders = new(); + [ObservableProperty] + private ObservableCollection checkpointFolders = new(); [ObservableProperty] private ObservableCollection displayedCheckpointFolders = new(); @@ -70,7 +73,8 @@ public partial class CheckpointsPageViewModel : PageViewModelBase // Set UI states IsImportAsConnected = settingsManager.Settings.IsImportAsConnected; - SearchFilter = string.Empty; + // Refresh search filter + OnSearchFilterChanged(string.Empty); if (Design.IsDesignMode) return; @@ -81,6 +85,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase IsIndexing = false; } + // ReSharper disable once UnusedParameterInPartialMethod partial void OnSearchFilterChanged(string value) { var filteredFolders = CheckpointFolders @@ -92,7 +97,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase DisplayedCheckpointFolders = new ObservableCollection(filteredFolders); } - + private bool ContainsSearchFilter(CheckpointFolder folder) { if (folder == null) diff --git a/StabilityMatrix.Avalonia/ViewModels/ViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/ViewModelBase.cs index 7445481b..30c81572 100644 --- a/StabilityMatrix.Avalonia/ViewModels/ViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/ViewModelBase.cs @@ -1,10 +1,28 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; +using AsyncAwaitBestPractices; using CommunityToolkit.Mvvm.ComponentModel; +using StabilityMatrix.Avalonia.Models; namespace StabilityMatrix.Avalonia.ViewModels; -public class ViewModelBase : ObservableValidator +public class ViewModelBase : ObservableValidator, IRemovableListItem { + private WeakEventManager? parentListRemoveRequestedEventManager; + + public event EventHandler ParentListRemoveRequested + { + add + { + parentListRemoveRequestedEventManager ??= new WeakEventManager(); + parentListRemoveRequestedEventManager.AddEventHandler(value); + } + remove => parentListRemoveRequestedEventManager?.RemoveEventHandler(value); + } + + protected void RemoveFromParentList() => parentListRemoveRequestedEventManager?.RaiseEvent( + this, EventArgs.Empty, nameof(ParentListRemoveRequested)); + public virtual void OnLoaded() { diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml index 97b4a7ee..44590bbf 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml @@ -216,6 +216,11 @@ + +