using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Linq; using System.Reactive; using System.Threading; using System.Threading.Tasks; using Artemis.UI.Screens.Workshop.Entries; using Artemis.UI.Screens.Workshop.Parameters; using Artemis.UI.Screens.Workshop.SubmissionWizard; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Utilities; using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop.Exceptions; using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.UploadHandlers; using Avalonia.Media.Imaging; using ReactiveUI; using StrawberryShake; namespace Artemis.UI.Screens.Workshop.Library; public class SubmissionDetailViewModel : RoutableScreen { private readonly IWorkshopClient _client; private readonly Func _getGetSpecificationsVm; private readonly IRouter _router; private readonly IWindowService _windowService; private readonly IWorkshopService _workshopService; private IGetSubmittedEntryById_Entry? _entry; private EntrySpecificationsViewModel? _entrySpecificationsViewModel; private bool _hasChanges; public SubmissionDetailViewModel(IWorkshopClient client, IWindowService windowService, IWorkshopService workshopService, IRouter router, Func getSpecificationsVm) { _client = client; _windowService = windowService; _workshopService = workshopService; _router = router; _getGetSpecificationsVm = getSpecificationsVm; CreateRelease = ReactiveCommand.CreateFromTask(ExecuteCreateRelease); DeleteSubmission = ReactiveCommand.CreateFromTask(ExecuteDeleteSubmission); ViewWorkshopPage = ReactiveCommand.CreateFromTask(ExecuteViewWorkshopPage); DiscardChanges = ReactiveCommand.CreateFromTask(ExecuteDiscardChanges, this.WhenAnyValue(vm => vm.HasChanges)); SaveChanges = ReactiveCommand.CreateFromTask(ExecuteSaveChanges, this.WhenAnyValue(vm => vm.HasChanges)); } public ReactiveCommand CreateRelease { get; } public ReactiveCommand DeleteSubmission { get; } public ReactiveCommand ViewWorkshopPage { get; } public ReactiveCommand SaveChanges { get; } public ReactiveCommand DiscardChanges { get; } public EntrySpecificationsViewModel? EntrySpecificationsViewModel { get => _entrySpecificationsViewModel; set => RaiseAndSetIfChanged(ref _entrySpecificationsViewModel, value); } public IGetSubmittedEntryById_Entry? Entry { get => _entry; set => RaiseAndSetIfChanged(ref _entry, value); } public bool HasChanges { get => _hasChanges; private set => RaiseAndSetIfChanged(ref _hasChanges, value); } public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) { IOperationResult result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken); if (result.IsErrorResult()) return; Entry = result.Data?.Entry; await ApplyFromEntry(cancellationToken); } public override 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(); } private async Task ApplyFromEntry(CancellationToken cancellationToken) { if (Entry == null) return; if (EntrySpecificationsViewModel != null) { EntrySpecificationsViewModel.PropertyChanged -= EntrySpecificationsViewModelOnPropertyChanged; ((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged -= SelectedCategoriesOnCollectionChanged; EntrySpecificationsViewModel.Tags.CollectionChanged -= TagsOnCollectionChanged; } EntrySpecificationsViewModel viewModel = _getGetSpecificationsVm(); viewModel.IconBitmap = await GetEntryIcon(cancellationToken); viewModel.Name = Entry.Name; viewModel.Summary = Entry.Summary; viewModel.Description = Entry.Description; viewModel.PreselectedCategories = Entry.Categories.Select(c => c.Id).ToList(); viewModel.Tags.Clear(); foreach (string tag in Entry.Tags.Select(c => c.Name)) viewModel.Tags.Add(tag); EntrySpecificationsViewModel = viewModel; EntrySpecificationsViewModel.PropertyChanged += EntrySpecificationsViewModelOnPropertyChanged; ((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged += SelectedCategoriesOnCollectionChanged; EntrySpecificationsViewModel.Tags.CollectionChanged += TagsOnCollectionChanged; } private async Task 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 categories = EntrySpecificationsViewModel.Categories.Where(c => c.IsSelected).Select(c => c.Id).OrderBy(c => c).ToList(); List tags = EntrySpecificationsViewModel.Tags.OrderBy(t => t).ToList(); HasChanges = EntrySpecificationsViewModel.Name != Entry.Name || EntrySpecificationsViewModel.Description != Entry.Description || EntrySpecificationsViewModel.Summary != Entry.Summary || EntrySpecificationsViewModel.IconChanged || !tags.SequenceEqual(Entry.Tags.Select(t => t.Name).OrderBy(t => t)) || !categories.SequenceEqual(Entry.Categories.Select(c => c.Id).OrderBy(c => c)); } private async Task ExecuteDiscardChanges() { await ApplyFromEntry(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, Categories = EntrySpecificationsViewModel.SelectedCategories, Tags = EntrySpecificationsViewModel.Tags }; IOperationResult result = await _client.UpdateEntry.ExecuteAsync(input, cancellationToken); result.EnsureNoErrors(); if (EntrySpecificationsViewModel.IconChanged && EntrySpecificationsViewModel.IconBitmap != null) { using MemoryStream stream = new(); EntrySpecificationsViewModel.IconBitmap.Save(stream); ImageUploadResult imageResult = await _workshopService.SetEntryIcon(Entry.Id, new Progress(), stream, cancellationToken); if (!imageResult.IsSuccess) throw new ArtemisWorkshopException("Failed to upload image. " + imageResult.Message); } HasChanges = false; } private async Task ExecuteCreateRelease(CancellationToken cancellationToken) { if (Entry != null) await _windowService.ShowDialogAsync(Entry); } private async Task ExecuteDeleteSubmission(CancellationToken cancellationToken) { if (Entry == null) return; bool confirmed = await _windowService.ShowConfirmContentDialog( "Delete submission?", "You cannot undo this by yourself.\r\n" + "Users that have already downloaded your submission will keep it."); if (!confirmed) return; IOperationResult result = await _client.RemoveEntry.ExecuteAsync(Entry.Id, cancellationToken); result.EnsureNoErrors(); await _router.Navigate("workshop/library/submissions"); } private async Task ExecuteViewWorkshopPage() { if (Entry != null) await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType); } private void TagsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { UpdateHasChanges(); } private void SelectedCategoriesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { UpdateHasChanges(); } private void EntrySpecificationsViewModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e) { UpdateHasChanges(); } }