mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-12 21:38:38 +00:00
Workshop - Allow editing other user's entries (admin only) Workshop - Show last update date in entry list instead of creation date
274 lines
11 KiB
C#
274 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reactive;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Artemis.UI.Screens.Workshop.Image;
|
|
using Artemis.UI.Shared;
|
|
using Artemis.UI.Shared.Routing;
|
|
using Artemis.UI.Shared.Services;
|
|
using Artemis.WebClient.Workshop;
|
|
using Artemis.WebClient.Workshop.Exceptions;
|
|
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
|
using Artemis.WebClient.Workshop.Services;
|
|
using Avalonia.Media.Imaging;
|
|
using FluentAvalonia.UI.Controls;
|
|
using PropertyChanged.SourceGenerator;
|
|
using ReactiveUI;
|
|
using StrawberryShake;
|
|
using EntrySpecificationsViewModel = Artemis.UI.Screens.Workshop.Entries.Details.EntrySpecificationsViewModel;
|
|
|
|
namespace Artemis.UI.Screens.Workshop.Library;
|
|
|
|
public partial class SubmissionDetailsViewModel : RoutableScreen
|
|
{
|
|
private readonly IWorkshopClient _client;
|
|
private readonly IWindowService _windowService;
|
|
private readonly IWorkshopService _workshopService;
|
|
private readonly IRouter _router;
|
|
private readonly Func<EntrySpecificationsViewModel> _getGetSpecificationsViewModel;
|
|
private readonly Func<IImage, ImageSubmissionViewModel> _getExistingImageSubmissionViewModel;
|
|
private readonly Func<ImageUploadRequest, ImageSubmissionViewModel> _getImageSubmissionViewModel;
|
|
private readonly List<ImageSubmissionViewModel> _removedImages = new();
|
|
|
|
[Notify] private IGetSubmittedEntryById_Entry? _entry;
|
|
[Notify] private EntrySpecificationsViewModel? _entrySpecificationsViewModel;
|
|
[Notify(Setter.Private)] private bool _hasChanges;
|
|
|
|
public SubmissionDetailsViewModel(IWorkshopClient client,
|
|
IWindowService windowService,
|
|
IWorkshopService workshopService,
|
|
IRouter router,
|
|
Func<EntrySpecificationsViewModel> getSpecificationsViewModel,
|
|
Func<IImage, ImageSubmissionViewModel> getExistingImageSubmissionViewModel,
|
|
Func<ImageUploadRequest, ImageSubmissionViewModel> getImageSubmissionViewModel)
|
|
{
|
|
_client = client;
|
|
_windowService = windowService;
|
|
_workshopService = workshopService;
|
|
_router = router;
|
|
_getGetSpecificationsViewModel = getSpecificationsViewModel;
|
|
_getExistingImageSubmissionViewModel = getExistingImageSubmissionViewModel;
|
|
_getImageSubmissionViewModel = getImageSubmissionViewModel;
|
|
|
|
AddImage = ReactiveCommand.CreateFromTask(ExecuteAddImage);
|
|
DiscardChanges = ReactiveCommand.CreateFromTask(ExecuteDiscardChanges, this.WhenAnyValue(vm => vm.HasChanges));
|
|
SaveChanges = ReactiveCommand.CreateFromTask(ExecuteSaveChanges, this.WhenAnyValue(vm => vm.HasChanges));
|
|
}
|
|
|
|
public ObservableCollection<ImageSubmissionViewModel> Images { get; } = new();
|
|
public ReactiveCommand<Unit, Unit> AddImage { get; }
|
|
public ReactiveCommand<Unit, Unit> SaveChanges { get; }
|
|
public ReactiveCommand<Unit, Unit> DiscardChanges { get; }
|
|
|
|
public async Task SetEntry(IGetSubmittedEntryById_Entry? entry, CancellationToken cancellationToken)
|
|
{
|
|
Entry = entry;
|
|
await ApplyDetailsFromEntry(cancellationToken);
|
|
}
|
|
|
|
public async Task OnClosing(NavigationArguments args)
|
|
{
|
|
if (!HasChanges)
|
|
return;
|
|
|
|
bool confirmed = await _windowService.ShowConfirmContentDialog("You have unsaved changes", "Do you want to discard your unsaved changes?");
|
|
if (!confirmed)
|
|
args.Cancel();
|
|
else
|
|
await ExecuteDiscardChanges();
|
|
}
|
|
|
|
private async Task ApplyDetailsFromEntry(CancellationToken cancellationToken)
|
|
{
|
|
// Clean up event handlers
|
|
if (EntrySpecificationsViewModel != null)
|
|
{
|
|
EntrySpecificationsViewModel.PropertyChanged -= InputChanged;
|
|
((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged -= InputChanged;
|
|
EntrySpecificationsViewModel.Tags.CollectionChanged -= InputChanged;
|
|
}
|
|
|
|
if (Entry == null)
|
|
{
|
|
EntrySpecificationsViewModel = null;
|
|
ApplyImagesFromEntry();
|
|
return;
|
|
}
|
|
|
|
EntrySpecificationsViewModel specificationsViewModel = _getGetSpecificationsViewModel();
|
|
specificationsViewModel.IconBitmap = await GetEntryIcon(cancellationToken);
|
|
specificationsViewModel.Name = Entry.Name;
|
|
specificationsViewModel.Summary = Entry.Summary;
|
|
specificationsViewModel.Description = Entry.Description;
|
|
specificationsViewModel.IsDefault = Entry.IsDefault;
|
|
specificationsViewModel.PreselectedCategories = Entry.Categories.Select(c => c.Id).ToList();
|
|
|
|
specificationsViewModel.Tags.Clear();
|
|
foreach (string tag in Entry.Tags.Select(c => c.Name))
|
|
specificationsViewModel.Tags.Add(tag);
|
|
|
|
EntrySpecificationsViewModel = specificationsViewModel;
|
|
EntrySpecificationsViewModel.PropertyChanged += InputChanged;
|
|
((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged += InputChanged;
|
|
EntrySpecificationsViewModel.Tags.CollectionChanged += InputChanged;
|
|
|
|
ApplyImagesFromEntry();
|
|
}
|
|
|
|
private void ApplyImagesFromEntry()
|
|
{
|
|
foreach (ImageSubmissionViewModel imageSubmissionViewModel in Images)
|
|
imageSubmissionViewModel.PropertyChanged -= InputChanged;
|
|
|
|
Images.Clear();
|
|
_removedImages.Clear();
|
|
|
|
if (Entry == null)
|
|
return;
|
|
|
|
foreach (IImage image in Entry.Images)
|
|
AddImageViewModel(_getExistingImageSubmissionViewModel(image));
|
|
}
|
|
|
|
private void AddImageViewModel(ImageSubmissionViewModel viewModel)
|
|
{
|
|
viewModel.PropertyChanged += InputChanged;
|
|
viewModel.Remove = ReactiveCommand.Create(() =>
|
|
{
|
|
// _removedImages is a list of images that are to be deleted, images without an ID never existed in the first place so only add those with an ID
|
|
if (viewModel.Id != null)
|
|
_removedImages.Add(viewModel);
|
|
|
|
Images.Remove(viewModel);
|
|
UpdateHasChanges();
|
|
});
|
|
Images.Add(viewModel);
|
|
}
|
|
|
|
private async Task<Bitmap?> GetEntryIcon(CancellationToken cancellationToken)
|
|
{
|
|
if (Entry == null)
|
|
return null;
|
|
|
|
Stream? stream = await _workshopService.GetEntryIcon(Entry.Id, cancellationToken);
|
|
return stream != null ? new Bitmap(stream) : null;
|
|
}
|
|
|
|
private void UpdateHasChanges()
|
|
{
|
|
if (EntrySpecificationsViewModel == null || Entry == null)
|
|
return;
|
|
|
|
List<long> categories = EntrySpecificationsViewModel.Categories.Where(c => c.IsSelected).Select(c => c.Id).OrderBy(c => c).ToList();
|
|
List<string> tags = EntrySpecificationsViewModel.Tags.OrderBy(t => t).ToList();
|
|
|
|
HasChanges = EntrySpecificationsViewModel.Name != Entry.Name ||
|
|
EntrySpecificationsViewModel.Description != Entry.Description ||
|
|
EntrySpecificationsViewModel.Summary != Entry.Summary ||
|
|
EntrySpecificationsViewModel.IsDefault != Entry.IsDefault ||
|
|
EntrySpecificationsViewModel.IconChanged ||
|
|
!tags.SequenceEqual(Entry.Tags.Select(t => t.Name).OrderBy(t => t)) ||
|
|
!categories.SequenceEqual(Entry.Categories.Select(c => c.Id).OrderBy(c => c)) ||
|
|
Images.Any(i => i.HasChanges) ||
|
|
_removedImages.Any();
|
|
}
|
|
|
|
private async Task ExecuteDiscardChanges()
|
|
{
|
|
await ApplyDetailsFromEntry(CancellationToken.None);
|
|
}
|
|
|
|
private async Task ExecuteSaveChanges(CancellationToken cancellationToken)
|
|
{
|
|
if (Entry == null || EntrySpecificationsViewModel == null || !EntrySpecificationsViewModel.ValidationContext.GetIsValid())
|
|
return;
|
|
|
|
UpdateEntryInput input = new()
|
|
{
|
|
Id = Entry.Id,
|
|
Name = EntrySpecificationsViewModel.Name,
|
|
Summary = EntrySpecificationsViewModel.Summary,
|
|
Description = EntrySpecificationsViewModel.Description,
|
|
IsDefault = EntrySpecificationsViewModel.IsDefault,
|
|
Categories = EntrySpecificationsViewModel.SelectedCategories,
|
|
Tags = EntrySpecificationsViewModel.Tags
|
|
};
|
|
|
|
IOperationResult<IUpdateEntryResult> result = await _client.UpdateEntry.ExecuteAsync(input, cancellationToken);
|
|
result.EnsureNoErrors();
|
|
|
|
if (EntrySpecificationsViewModel.IconChanged && EntrySpecificationsViewModel.IconBitmap != null)
|
|
{
|
|
using MemoryStream stream = new();
|
|
EntrySpecificationsViewModel.IconBitmap.Save(stream);
|
|
ApiResult imageResult = await _workshopService.SetEntryIcon(Entry.Id, stream, cancellationToken);
|
|
if (!imageResult.IsSuccess)
|
|
throw new ArtemisWorkshopException("Failed to upload image. " + imageResult.Message);
|
|
}
|
|
|
|
foreach (ImageSubmissionViewModel imageViewModel in Images)
|
|
{
|
|
// Upload new images
|
|
if (imageViewModel.ImageUploadRequest != null)
|
|
{
|
|
await _workshopService.UploadEntryImage(Entry.Id, imageViewModel.ImageUploadRequest, cancellationToken);
|
|
}
|
|
// Update existing images
|
|
else if (imageViewModel.HasChanges && imageViewModel.Id != null)
|
|
{
|
|
if (imageViewModel.Name != null)
|
|
await _client.UpdateEntryImage.ExecuteAsync(imageViewModel.Id.Value, imageViewModel.Name, imageViewModel.Description, cancellationToken);
|
|
}
|
|
}
|
|
|
|
// Delete old images
|
|
foreach (ImageSubmissionViewModel imageViewModel in _removedImages)
|
|
{
|
|
if (imageViewModel.Id != null)
|
|
await _workshopService.DeleteEntryImage(imageViewModel.Id.Value, cancellationToken);
|
|
}
|
|
|
|
HasChanges = false;
|
|
await _router.Reload();
|
|
}
|
|
|
|
private async Task ExecuteAddImage(CancellationToken arg)
|
|
{
|
|
string[]? result = await _windowService.CreateOpenFileDialog().WithAllowMultiple().HavingFilter(f => f.WithBitmaps()).ShowAsync();
|
|
if (result == null)
|
|
return;
|
|
|
|
foreach (string path in result)
|
|
{
|
|
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
|
|
if (stream.Length > ImageUploadRequest.MAX_FILE_SIZE)
|
|
{
|
|
await _windowService.ShowConfirmContentDialog("File too big", $"File {path} exceeds maximum file size of 10 MB", "Skip file", null);
|
|
await stream.DisposeAsync();
|
|
continue;
|
|
}
|
|
|
|
ImageUploadRequest request = new(stream, Path.GetFileName(path), string.Empty);
|
|
ImageSubmissionViewModel viewModel = _getImageSubmissionViewModel(request);
|
|
|
|
// Show the dialog to give the image a name and description
|
|
if (await viewModel.Edit() != ContentDialogResult.Primary)
|
|
{
|
|
await stream.DisposeAsync();
|
|
continue;
|
|
}
|
|
|
|
AddImageViewModel(viewModel);
|
|
}
|
|
}
|
|
|
|
private void InputChanged(object? sender, EventArgs e)
|
|
{
|
|
UpdateHasChanges();
|
|
}
|
|
} |