Browse Source

Add folder deletion context menu

pull/55/head
Ionite 1 year ago
parent
commit
0721337f4d
No known key found for this signature in database
  1. 4
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  2. 37
      StabilityMatrix.Avalonia/DialogHelper.cs
  3. 71
      StabilityMatrix.Avalonia/Models/AdvancedObservableList.cs
  4. 8
      StabilityMatrix.Avalonia/Models/IRemovableListItem.cs
  5. 8
      StabilityMatrix.Avalonia/ViewModels/CheckpointFile.cs
  6. 78
      StabilityMatrix.Avalonia/ViewModels/CheckpointFolder.cs
  7. 11
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  8. 22
      StabilityMatrix.Avalonia/ViewModels/ViewModelBase.cs
  9. 5
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

4
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -129,7 +129,7 @@ public static class DesignData
{ {
Title = "Lora", Title = "Lora",
DirectoryPath = "Packages/lora", DirectoryPath = "Packages/lora",
CheckpointFiles = new ObservableCollection<CheckpointFile> CheckpointFiles = new AdvancedObservableList<CheckpointFile>
{ {
new() new()
{ {
@ -162,7 +162,7 @@ public static class DesignData
{ {
Title = "VAE", Title = "VAE",
DirectoryPath = "Packages/VAE", DirectoryPath = "Packages/VAE",
CheckpointFiles = new ObservableCollection<CheckpointFile> CheckpointFiles = new AdvancedObservableList<CheckpointFile>
{ {
new() new()
{ {

37
StabilityMatrix.Avalonia/DialogHelper.cs

@ -4,8 +4,10 @@ using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
@ -132,6 +134,41 @@ public static class DialogHelper
IsPrimaryButtonEnabled = false, IsPrimaryButtonEnabled = false,
}; };
} }
/// <summary>
/// Create a simple title and description task dialog.
/// Sets the XamlRoot to the current top level window.
/// </summary>
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 // Text fields

71
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;
/// <summary>
/// Observable AvaloniaList supporting child item deletion requests.
/// </summary>
public class AdvancedObservableList<T> : AvaloniaList<T>
{
/// <inheritdoc />
public AdvancedObservableList()
{
CollectionChanged += CollectionChangedEventRegistrationHandler;
}
/// <inheritdoc />
public AdvancedObservableList(IEnumerable<T> 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;
}
}

8
StabilityMatrix.Avalonia/Models/IRemovableListItem.cs

@ -0,0 +1,8 @@
using System;
namespace StabilityMatrix.Avalonia.Models;
public interface IRemovableListItem
{
public event EventHandler ParentListRemoveRequested;
}

8
StabilityMatrix.Avalonia/ViewModels/CheckpointFile.cs

@ -5,10 +5,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
@ -25,9 +22,6 @@ public partial class CheckpointFile : ViewModelBase
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
// Event for when this file is deleted
public event EventHandler<CheckpointFile>? Deleted;
/// <summary> /// <summary>
/// Absolute path to the checkpoint file. /// Absolute path to the checkpoint file.
/// </summary> /// </summary>
@ -122,7 +116,7 @@ public partial class CheckpointFile : ViewModelBase
IsLoading = false; IsLoading = false;
} }
} }
Deleted?.Invoke(this, this); RemoveFromParentList();
} }
[RelayCommand] [RelayCommand]

78
StabilityMatrix.Avalonia/ViewModels/CheckpointFolder.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -9,10 +8,10 @@ using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Threading; using Avalonia.Threading;
using AvaloniaEdit.Utils;
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 StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
@ -77,9 +76,9 @@ public partial class CheckpointFolder : ViewModelBase
public ProgressViewModel Progress { get; } = new(); public ProgressViewModel Progress { get; } = new();
public CheckpointFolder? ParentFolder { get; init; } public CheckpointFolder? ParentFolder { get; init; }
public ObservableCollection<CheckpointFolder> SubFolders { get; init; } = new(); public AdvancedObservableList<CheckpointFolder> SubFolders { get; init; } = new();
public ObservableCollection<CheckpointFile> CheckpointFiles { get; init; } = new(); public AdvancedObservableList<CheckpointFile> CheckpointFiles { get; init; } = new();
public ObservableCollection<CheckpointFile> DisplayedCheckpointFiles { get; set; } public AdvancedObservableList<CheckpointFile> DisplayedCheckpointFiles { get; set; }
public CheckpointFolder( public CheckpointFolder(
ISettingsManager settingsManager, ISettingsManager settingsManager,
@ -121,7 +120,7 @@ public partial class CheckpointFolder : ViewModelBase
{ {
var filteredFiles = CheckpointFiles.Where(y => var filteredFiles = CheckpointFiles.Where(y =>
y.FileName.Contains(value, StringComparison.OrdinalIgnoreCase)); y.FileName.Contains(value, StringComparison.OrdinalIgnoreCase));
DisplayedCheckpointFiles = new ObservableCollection<CheckpointFile>(filteredFiles); DisplayedCheckpointFiles = new AdvancedObservableList<CheckpointFile>(filteredFiles);
} }
} }
@ -137,30 +136,9 @@ public partial class CheckpointFolder : ViewModelBase
} }
} }
// On collection changes
private void OnCheckpointFilesChanged(object? sender, NotifyCollectionChangedEventArgs e) private void OnCheckpointFilesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
OnPropertyChanged(nameof(TitleWithFilesCount)); 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;
}
}
/// <summary>
/// Handler for CheckpointFile requesting to be deleted from the collection.
/// </summary>
/// <param name="sender"></param>
/// <param name="file"></param>
private void OnCheckpointFileDelete(object? sender, CheckpointFile file)
{
Dispatcher.UIThread.Post(() =>
{
CheckpointFiles.Remove(file);
DisplayedCheckpointFiles.Remove(file);
});
} }
public async Task OnDrop(DragEventArgs e) public async Task OnDrop(DragEventArgs e)
@ -190,6 +168,52 @@ public partial class CheckpointFolder : ViewModelBase
await ProcessRunner.OpenFolderBrowser(path); 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>
{
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] [RelayCommand]
private async Task CreateSubFolder() private async Task CreateSubFolder()
{ {

11
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

@ -36,7 +36,9 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
[ObservableProperty] private bool isImportAsConnected; [ObservableProperty] private bool isImportAsConnected;
[ObservableProperty] private bool isLoading; [ObservableProperty] private bool isLoading;
[ObservableProperty] private bool isIndexing; [ObservableProperty] private bool isIndexing;
[ObservableProperty] private string searchFilter;
[ObservableProperty]
private string searchFilter = string.Empty;
partial void OnIsImportAsConnectedChanged(bool value) partial void OnIsImportAsConnectedChanged(bool value)
{ {
@ -47,7 +49,8 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
} }
} }
[ObservableProperty] private ObservableCollection<CheckpointFolder> checkpointFolders = new(); [ObservableProperty]
private ObservableCollection<CheckpointFolder> checkpointFolders = new();
[ObservableProperty] [ObservableProperty]
private ObservableCollection<CheckpointFolder> displayedCheckpointFolders = new(); private ObservableCollection<CheckpointFolder> displayedCheckpointFolders = new();
@ -70,7 +73,8 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
// Set UI states // Set UI states
IsImportAsConnected = settingsManager.Settings.IsImportAsConnected; IsImportAsConnected = settingsManager.Settings.IsImportAsConnected;
SearchFilter = string.Empty; // Refresh search filter
OnSearchFilterChanged(string.Empty);
if (Design.IsDesignMode) return; if (Design.IsDesignMode) return;
@ -81,6 +85,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
IsIndexing = false; IsIndexing = false;
} }
// ReSharper disable once UnusedParameterInPartialMethod
partial void OnSearchFilterChanged(string value) partial void OnSearchFilterChanged(string value)
{ {
var filteredFolders = CheckpointFolders var filteredFolders = CheckpointFolders

22
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 CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Models;
namespace StabilityMatrix.Avalonia.ViewModels; 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() public virtual void OnLoaded()
{ {

5
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -216,6 +216,11 @@
<ui:MenuFlyoutItem Text="Show in Explorer" IconSource="Open" <ui:MenuFlyoutItem Text="Show in Explorer" IconSource="Open"
Command="{Binding ShowInExplorerCommand}" Command="{Binding ShowInExplorerCommand}"
CommandParameter="{Binding DirectoryPath}"/> CommandParameter="{Binding DirectoryPath}"/>
<!-- Only allow deletion of non-root folders (Parent is not null) -->
<ui:MenuFlyoutItem Text="Delete" IconSource="Delete"
IsEnabled="{Binding ParentFolder, Converter={x:Static ObjectConverters.IsNotNull}}"
IsVisible="{Binding ParentFolder, Converter={x:Static ObjectConverters.IsNotNull}}"
Command="{Binding DeleteCommand}"/>
<ui:MenuFlyoutSeparator/> <ui:MenuFlyoutSeparator/>
<ui:MenuFlyoutSubItem Text="New" IconSource="Add"> <ui:MenuFlyoutSubItem Text="New" IconSource="Add">
<ui:MenuFlyoutSubItem.Items> <ui:MenuFlyoutSubItem.Items>

Loading…
Cancel
Save