You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
12 KiB
362 lines
12 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Text.Json; |
|
using System.Text.Json.Serialization; |
|
using System.Threading.Tasks; |
|
using AsyncAwaitBestPractices; |
|
using Avalonia.Controls; |
|
using Avalonia.Controls.Notifications; |
|
using Avalonia.Input; |
|
using Avalonia.Platform.Storage; |
|
using Avalonia.Threading; |
|
using CommunityToolkit.Mvvm.ComponentModel; |
|
using CommunityToolkit.Mvvm.Input; |
|
using FluentAvalonia.UI.Controls; |
|
using FluentAvalonia.UI.Media.Animation; |
|
using Microsoft.Extensions.DependencyInjection; |
|
using NLog; |
|
using StabilityMatrix.Avalonia.Animations; |
|
using StabilityMatrix.Avalonia.Models; |
|
using StabilityMatrix.Avalonia.Models.Inference; |
|
using StabilityMatrix.Avalonia.Services; |
|
using StabilityMatrix.Avalonia.ViewModels.Inference; |
|
using StabilityMatrix.Core.Helper; |
|
using StabilityMatrix.Core.Models; |
|
using StabilityMatrix.Core.Models.Database; |
|
using StabilityMatrix.Core.Models.FileInterfaces; |
|
|
|
#pragma warning disable CS0657 // Not a valid attribute location for this declaration |
|
|
|
namespace StabilityMatrix.Avalonia.ViewModels.Base; |
|
|
|
public abstract partial class InferenceTabViewModelBase |
|
: LoadableViewModelBase, |
|
IDisposable, |
|
IPersistentViewProvider, |
|
IDropTarget |
|
{ |
|
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
|
|
|
private readonly INotificationService notificationService; |
|
|
|
/// <summary> |
|
/// The title of the tab |
|
/// </summary> |
|
public virtual string TabTitle => ProjectFile?.NameWithoutExtension ?? "New Project"; |
|
|
|
/// <summary> |
|
/// Whether there are unsaved changes |
|
/// </summary> |
|
[ObservableProperty] |
|
[property: JsonIgnore] |
|
private bool hasUnsavedChanges; |
|
|
|
/// <summary> |
|
/// The tab's project file |
|
/// </summary> |
|
[ObservableProperty] |
|
[NotifyPropertyChangedFor(nameof(TabTitle))] |
|
[property: JsonIgnore] |
|
private FilePath? projectFile; |
|
|
|
/// <inheritdoc /> |
|
Control? IPersistentViewProvider.AttachedPersistentView { get; set; } |
|
|
|
#region Weak Events |
|
|
|
private WeakEventManager<LoadViewStateEventArgs>? loadViewStateRequestedEventManager; |
|
|
|
public event EventHandler<LoadViewStateEventArgs> LoadViewStateRequested |
|
{ |
|
add |
|
{ |
|
loadViewStateRequestedEventManager ??= new WeakEventManager<LoadViewStateEventArgs>(); |
|
loadViewStateRequestedEventManager.AddEventHandler(value); |
|
} |
|
remove => loadViewStateRequestedEventManager?.RemoveEventHandler(value); |
|
} |
|
|
|
protected void LoadViewState(LoadViewStateEventArgs args) => |
|
loadViewStateRequestedEventManager?.RaiseEvent(this, args, nameof(LoadViewStateRequested)); |
|
|
|
protected void ResetViewState() => LoadViewState(new LoadViewStateEventArgs()); |
|
|
|
private WeakEventManager<SaveViewStateEventArgs>? saveViewStateRequestedEventManager; |
|
|
|
public event EventHandler<SaveViewStateEventArgs> SaveViewStateRequested |
|
{ |
|
add |
|
{ |
|
saveViewStateRequestedEventManager ??= new WeakEventManager<SaveViewStateEventArgs>(); |
|
saveViewStateRequestedEventManager.AddEventHandler(value); |
|
} |
|
remove => saveViewStateRequestedEventManager?.RemoveEventHandler(value); |
|
} |
|
|
|
protected async Task<ViewState> SaveViewState() |
|
{ |
|
var eventArgs = new SaveViewStateEventArgs(); |
|
saveViewStateRequestedEventManager?.RaiseEvent( |
|
this, |
|
eventArgs, |
|
nameof(SaveViewStateRequested) |
|
); |
|
|
|
if (eventArgs.StateTask is not { } stateTask) |
|
{ |
|
throw new InvalidOperationException( |
|
"SaveViewStateRequested event handler did not set the StateTask property" |
|
); |
|
} |
|
|
|
return await stateTask; |
|
} |
|
|
|
#endregion |
|
|
|
protected InferenceTabViewModelBase(INotificationService notificationService) |
|
{ |
|
this.notificationService = notificationService; |
|
} |
|
|
|
[RelayCommand] |
|
private void RestoreDefaultViewState() |
|
{ |
|
// ResetViewState(); |
|
// TODO: Dock reset not working, using this hack for now to get a new view |
|
|
|
var navService = App.Services.GetRequiredService<INavigationService<MainWindowViewModel>>(); |
|
navService.NavigateTo<LaunchPageViewModel>(new SuppressNavigationTransitionInfo()); |
|
((IPersistentViewProvider)this).AttachedPersistentView = null; |
|
navService.NavigateTo<InferenceViewModel>(new BetterEntranceNavigationTransition()); |
|
} |
|
|
|
[RelayCommand] |
|
private async Task DebugSaveViewState() |
|
{ |
|
var state = await SaveViewState(); |
|
if (state.DockLayout is { } layout) |
|
{ |
|
await DialogHelper.CreateJsonDialog(layout).ShowAsync(); |
|
} |
|
else |
|
{ |
|
await DialogHelper.CreateTaskDialog("Failed", "No layout data").ShowAsync(); |
|
} |
|
} |
|
|
|
[RelayCommand] |
|
private async Task DebugLoadViewState() |
|
{ |
|
var textFields = new TextBoxField[] { new() { Label = "Json Data" } }; |
|
|
|
var dialog = DialogHelper.CreateTextEntryDialog("Load Dock State", "", textFields); |
|
var result = await dialog.ShowAsync(); |
|
|
|
if (result == ContentDialogResult.Primary && textFields[0].Text is { } json) |
|
{ |
|
LoadViewState( |
|
new LoadViewStateEventArgs { State = new ViewState { DockLayout = json } } |
|
); |
|
} |
|
} |
|
|
|
protected virtual void Dispose(bool disposing) |
|
{ |
|
if (disposing) |
|
{ |
|
((IPersistentViewProvider)this).AttachedPersistentView = null; |
|
} |
|
} |
|
|
|
/// <inheritdoc /> |
|
public void Dispose() |
|
{ |
|
Dispose(true); |
|
GC.SuppressFinalize(this); |
|
} |
|
|
|
/// <summary> |
|
/// Loads image and metadata from a file path |
|
/// </summary> |
|
/// <remarks>This is safe to call from non-UI threads</remarks> |
|
/// <param name="filePath">File path</param> |
|
/// <exception cref="FileNotFoundException"></exception> |
|
/// <exception cref="ApplicationException"></exception> |
|
/// <exception cref="InvalidOperationException"></exception> |
|
private void LoadImageMetadata(FilePath? filePath) |
|
{ |
|
if (filePath is not { Exists: true }) |
|
{ |
|
throw new FileNotFoundException("File does not exist", filePath?.FullPath); |
|
} |
|
|
|
var metadata = ImageMetadata.GetAllFileMetadata(filePath); |
|
|
|
// Has SMProject metadata |
|
if (metadata.SMProject is not null) |
|
{ |
|
var project = JsonSerializer.Deserialize<InferenceProjectDocument>(metadata.SMProject); |
|
|
|
// Check project type matches |
|
if (project?.ProjectType.ToViewModelType() == GetType() && project.State is not null) |
|
{ |
|
Dispatcher.UIThread.Invoke(() => LoadStateFromJsonObject(project.State)); |
|
} |
|
else |
|
{ |
|
throw new ApplicationException("Unsupported project type"); |
|
} |
|
|
|
// Load image |
|
if (this is IImageGalleryComponent imageGalleryComponent) |
|
{ |
|
imageGalleryComponent.LoadImagesToGallery(new ImageSource(filePath)); |
|
} |
|
} |
|
// Has generic metadata |
|
else if (metadata.Parameters is { } parametersString) |
|
{ |
|
if (!GenerationParameters.TryParse(parametersString, out var parameters)) |
|
{ |
|
throw new ApplicationException("Failed to parse parameters"); |
|
} |
|
|
|
if (this is IParametersLoadableState paramsLoadableVm) |
|
{ |
|
Dispatcher.UIThread.Invoke( |
|
() => paramsLoadableVm.LoadStateFromParameters(parameters) |
|
); |
|
} |
|
else |
|
{ |
|
throw new InvalidOperationException( |
|
"Load parameters target does not implement IParametersLoadableState" |
|
); |
|
} |
|
|
|
// Load image |
|
if (this is IImageGalleryComponent imageGalleryComponent) |
|
{ |
|
Dispatcher.UIThread.Invoke( |
|
() => imageGalleryComponent.LoadImagesToGallery(new ImageSource(filePath)) |
|
); |
|
} |
|
} |
|
else |
|
{ |
|
throw new ApplicationException("File does not contain any metadata"); |
|
} |
|
} |
|
|
|
/// <inheritdoc /> |
|
public void DragOver(object? sender, DragEventArgs e) |
|
{ |
|
// 1. Context drop for LocalImageFile |
|
if (e.Data.GetDataFormats().Contains("Context")) |
|
{ |
|
if (e.Data.Get("Context") is LocalImageFile imageFile) |
|
{ |
|
e.Handled = true; |
|
return; |
|
} |
|
|
|
e.DragEffects = DragDropEffects.None; |
|
} |
|
// 2. OS Files |
|
if (e.Data.GetDataFormats().Contains(DataFormats.Files)) |
|
{ |
|
e.Handled = true; |
|
return; |
|
} |
|
|
|
// Other kinds - not supported |
|
e.DragEffects = DragDropEffects.None; |
|
} |
|
|
|
/// <inheritdoc /> |
|
public void Drop(object? sender, DragEventArgs e) |
|
{ |
|
// 1. Context drop for LocalImageFile |
|
if (e.Data.GetDataFormats().Contains("Context")) |
|
{ |
|
if (e.Data.Get("Context") is LocalImageFile imageFile) |
|
{ |
|
e.Handled = true; |
|
|
|
Dispatcher.UIThread.Post(() => |
|
{ |
|
try |
|
{ |
|
var metadata = imageFile.ReadMetadata(); |
|
if (metadata.SMProject is not null) |
|
{ |
|
var project = JsonSerializer.Deserialize<InferenceProjectDocument>( |
|
metadata.SMProject |
|
); |
|
|
|
// Check project type matches |
|
if ( |
|
project?.ProjectType.ToViewModelType() == GetType() |
|
&& project.State is not null |
|
) |
|
{ |
|
LoadStateFromJsonObject(project.State); |
|
} |
|
|
|
// Load image |
|
if (this is IImageGalleryComponent imageGalleryComponent) |
|
{ |
|
imageGalleryComponent.LoadImagesToGallery( |
|
new ImageSource(imageFile.AbsolutePath) |
|
); |
|
} |
|
} |
|
} |
|
catch (Exception ex) |
|
{ |
|
Logger.Warn(ex, "Failed to load image from context drop"); |
|
notificationService.ShowPersistent( |
|
$"Could not parse image metadata", |
|
$"{imageFile.FileName} - {ex.Message}", |
|
NotificationType.Warning |
|
); |
|
} |
|
}); |
|
|
|
return; |
|
} |
|
} |
|
// 2. OS Files |
|
if (e.Data.GetDataFormats().Contains(DataFormats.Files)) |
|
{ |
|
e.Handled = true; |
|
|
|
if (e.Data.Get(DataFormats.Files) is IEnumerable<IStorageItem> files) |
|
{ |
|
if (files.Select(f => f.TryGetLocalPath()).FirstOrDefault() is { } path) |
|
{ |
|
var file = new FilePath(path); |
|
Dispatcher.UIThread.Post(() => |
|
{ |
|
try |
|
{ |
|
LoadImageMetadata(file); |
|
} |
|
catch (Exception ex) |
|
{ |
|
Logger.Warn(ex, "Failed to load image from OS file drop"); |
|
notificationService.ShowPersistent( |
|
$"Could not parse image metadata", |
|
$"{file.Name} - {ex.Message}", |
|
NotificationType.Warning |
|
); |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
} |
|
}
|
|
|