mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 11:56:30 +00:00
Workshop - Simplify upload to a busy indicator, show images in profiles
This commit is contained in:
1 parent
e33ae8a066
commit
e304d67035
22 files changed
+125
-242
No files matched your search
@@ -7,18 +7,25 @@
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntryImageView"
|
||||
x:DataType="details:EntryImageViewModel">
|
||||
<Border Classes="card" Padding="0" Width="300" ClipToBounds="True" Margin="0 5 0 0">
|
||||
<Border Classes="card" Padding="0">
|
||||
<Grid RowDefinitions="230,*">
|
||||
<Rectangle Grid.Row="0" Fill="{DynamicResource CheckerboardBrush}" />
|
||||
<Image Grid.Row="0"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||
asyncImageLoader:ImageLoader.Source="{CompiledBinding ThumbnailUrl, Mode=OneWay}" />
|
||||
<StackPanel Grid.Row="1" Margin="12">
|
||||
<TextBlock Text="{CompiledBinding Image.Name}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="subtitle" Text="{CompiledBinding Image.Description}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Border Grid.Row="0" ClipToBounds="True" CornerRadius="4 4 0 0" Padding="0">
|
||||
<Rectangle RenderOptions.BitmapInterpolationMode="HighQuality">
|
||||
<Rectangle.Fill>
|
||||
<ImageBrush asyncImageLoader:ImageBrushLoader.Source="{CompiledBinding ThumbnailUrl}" Stretch="UniformToFill" />
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
</Border>
|
||||
<Border Grid.Row="1" ClipToBounds="True" CornerRadius="0 0 4 4" Background="{DynamicResource ControlFillColorDefaultBrush}">
|
||||
<StackPanel Margin="16">
|
||||
<TextBlock Text="{CompiledBinding Image.Name}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="subtitle"
|
||||
Text="{CompiledBinding Image.Description}"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{CompiledBinding Image.Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -1,8 +1,9 @@
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.WebClient.Workshop;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.Details;
|
||||
|
||||
public class EntryImageViewModel
|
||||
public class EntryImageViewModel : ViewModelBase
|
||||
{
|
||||
public EntryImageViewModel(IImage image)
|
||||
{
|
||||
|
||||
@@ -8,21 +8,23 @@
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntryImagesView"
|
||||
x:DataType="details:EntryImagesViewModel">
|
||||
<ItemsControl ItemsSource="{CompiledBinding Images}" Margin="0 -16 0 0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.DataTemplates>
|
||||
<DataTemplate x:DataType="details:EntryImageViewModel">
|
||||
<Border CornerRadius="6"
|
||||
Margin="0 16 0 0"
|
||||
MaxWidth="300"
|
||||
ClipToBounds="True">
|
||||
<Image Stretch="UniformToFill" asyncImageLoader:ImageLoader.Source="{CompiledBinding ThumbnailUrl, Mode=OneWay}" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.DataTemplates>
|
||||
</ItemsControl>
|
||||
<ScrollViewer Classes="with-padding" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{CompiledBinding Images}" Width="300">
|
||||
<ItemsControl.Styles>
|
||||
<Styles>
|
||||
<Style Selector="ItemsControl > ContentPresenter">
|
||||
<Setter Property="Margin" Value="0 0 0 10"></Setter>
|
||||
</Style>
|
||||
<Style Selector="ItemsControl > ContentPresenter:nth-last-child(1)">
|
||||
<Setter Property="Margin" Value="0 0 0 0"></Setter>
|
||||
</Style>
|
||||
</Styles>
|
||||
</ItemsControl.Styles>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -23,7 +23,7 @@
|
||||
</Styles>
|
||||
</controls:NavigationView.Styles>
|
||||
|
||||
<controls:Frame Name="TabFrame" IsNavigationStackEnabled="False" CacheSize="0" Padding="20">
|
||||
<controls:Frame Name="TabFrame" IsNavigationStackEnabled="False" CacheSize="0" Padding="20 20 10 20">
|
||||
<controls:Frame.NavigationPageFactory>
|
||||
<ui:PageFactory/>
|
||||
</controls:Frame.NavigationPageFactory>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Image;
|
||||
|
||||
public partial class ImagePropertiesDialogView : UserControl
|
||||
public partial class ImagePropertiesDialogView : ReactiveUserControl<ImagePropertiesDialogViewModel>
|
||||
{
|
||||
public ImagePropertiesDialogView()
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
Source="{CompiledBinding Bitmap}" />
|
||||
<StackPanel Grid.Row="1" Margin="12">
|
||||
<TextBlock Text="{CompiledBinding Name, FallbackValue=Unnamed image}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock TextWrapping="Wrap" Classes="subtitle" Text="{CompiledBinding Description}" />
|
||||
<TextBlock TextWrapping="Wrap" Classes="subtitle" Text="{CompiledBinding Description, FallbackValue='No description'}" />
|
||||
<Separator Margin="-4 10" />
|
||||
<TextBlock TextWrapping="Wrap" Classes="subtitle">
|
||||
<Run Text="{CompiledBinding ImageDimensions}" /> - <Run Text="{CompiledBinding FileSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay}" />
|
||||
|
||||
@@ -4,13 +4,14 @@ using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.Builders;
|
||||
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Validation.Extensions;
|
||||
using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Image;
|
||||
|
||||
@@ -30,30 +31,35 @@ public partial class ImageSubmissionViewModel : ValidatableViewModelBase
|
||||
_image = image;
|
||||
_windowService = windowService;
|
||||
|
||||
FileSize = _image.File.Length;
|
||||
Name = _image.Name;
|
||||
Description = _image.Description;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
_image.File.Seek(0, SeekOrigin.Begin);
|
||||
Bitmap = new Bitmap(_image.File);
|
||||
FileSize = _image.File.Length;
|
||||
ImageDimensions = Bitmap.Size.Width + "x" + Bitmap.Size.Height;
|
||||
Name = _image.Name;
|
||||
Description = _image.Description;
|
||||
|
||||
Bitmap.DisposeWith(d);
|
||||
}, DispatcherPriority.Background);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task Edit()
|
||||
public async Task<ContentDialogResult> Edit()
|
||||
{
|
||||
await _windowService.CreateContentDialog()
|
||||
ContentDialogResult result = await _windowService.CreateContentDialog()
|
||||
.WithTitle("Edit image properties")
|
||||
.WithViewModel(out ImagePropertiesDialogViewModel vm, _image)
|
||||
.HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm))
|
||||
.WithCloseButtonText("Cancel")
|
||||
.WithDefaultButton(ContentDialogButton.Primary)
|
||||
.ShowAsync();
|
||||
|
||||
Name = _image.Name;
|
||||
Description = _image.Description;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -158,7 +158,7 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
||||
{
|
||||
using MemoryStream stream = new();
|
||||
EntrySpecificationsViewModel.IconBitmap.Save(stream);
|
||||
ImageUploadResult imageResult = await _workshopService.SetEntryIcon(Entry.Id, new Progress<StreamProgress>(), stream, cancellationToken);
|
||||
ImageUploadResult imageResult = await _workshopService.SetEntryIcon(Entry.Id, stream, cancellationToken);
|
||||
if (!imageResult.IsSuccess)
|
||||
throw new ArtemisWorkshopException("Failed to upload image. " + imageResult.Message);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView"
|
||||
x:DataType="profile:ProfileDetailsViewModel">
|
||||
<Grid ColumnDefinitions="300,*, 300" RowDefinitions="Auto,*">
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" Margin="0 0 10 0" Spacing="10">
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" Spacing="10">
|
||||
<Border Classes="card" VerticalAlignment="Top">
|
||||
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
||||
</Border>
|
||||
@@ -17,7 +17,7 @@
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<Border Classes="card" Grid.Row="1" Grid.Column="1">
|
||||
<Border Classes="card" Grid.Row="1" Grid.Column="1" Margin="10 0">
|
||||
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
|
||||
<mdxaml:MarkdownScrollViewer.Styles>
|
||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
||||
@@ -25,8 +25,6 @@
|
||||
</mdxaml:MarkdownScrollViewer>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}">
|
||||
<ContentControl Content="{CompiledBinding EntryImagesViewModel}" />
|
||||
</StackPanel>
|
||||
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -9,6 +9,7 @@ using Artemis.UI.Screens.Workshop.Image;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||
using DynamicData;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
|
||||
@@ -49,7 +50,7 @@ public class ImagesStepViewModel : SubmissionViewModel
|
||||
private ImageSubmissionViewModel CreateImageSubmissionViewModel(ImageUploadRequest image)
|
||||
{
|
||||
ImageSubmissionViewModel viewModel = _getImageSubmissionViewModel(image);
|
||||
viewModel.Remove = ReactiveCommand.Create(() => _stateImages.Remove(image));
|
||||
viewModel.Remove = ReactiveCommand.Create(() => RemoveImage(image));
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
@@ -73,8 +74,23 @@ public class ImagesStepViewModel : SubmissionViewModel
|
||||
}
|
||||
|
||||
ImageUploadRequest request = new(stream, Path.GetFileName(path), string.Empty);
|
||||
_stateImages.Add(request);
|
||||
State.Images.Add(request);
|
||||
AddImage(request);
|
||||
|
||||
// Show the dialog to give the image a name and description
|
||||
if (await Images.Last().Edit() != ContentDialogResult.Primary)
|
||||
RemoveImage(request); // user did not click confirm, remove again
|
||||
}
|
||||
}
|
||||
|
||||
private void AddImage(ImageUploadRequest image)
|
||||
{
|
||||
_stateImages.Add(image);
|
||||
State.Images.Add(image);
|
||||
}
|
||||
|
||||
private void RemoveImage(ImageUploadRequest image)
|
||||
{
|
||||
_stateImages.Remove(image);
|
||||
State.Images.Remove(image);
|
||||
}
|
||||
}
|
||||
@@ -16,17 +16,11 @@
|
||||
</Styles>
|
||||
</StackPanel.Styles>
|
||||
|
||||
<TextBlock IsVisible="{CompiledBinding !Finished}" Theme="{StaticResource TitleTextBlockStyle}" TextAlignment="Center" TextWrapping="Wrap">
|
||||
Uploading your submission...
|
||||
</TextBlock>
|
||||
|
||||
<ProgressBar IsVisible="{CompiledBinding !Finished}"
|
||||
Margin="0 15 0 0"
|
||||
Width="380"
|
||||
IsIndeterminate="{CompiledBinding ProgressIndeterminate}"
|
||||
Value="{CompiledBinding ProgressPercentage}">
|
||||
</ProgressBar>
|
||||
|
||||
<StackPanel IsVisible="{CompiledBinding !Finished}">
|
||||
<Lottie Path="/Assets/Animations/busy.json" Width="250" Height="250" Margin="0 100" ></Lottie>
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Uploading your submission...</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel IsVisible="{CompiledBinding Succeeded}">
|
||||
<Lottie Path="/Assets/Animations/success.json" RepeatCount="1" Width="250" Height="250" Margin="0 100"></Lottie>
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">All done! Hit finish to view your submission.</TextBlock>
|
||||
|
||||
@@ -23,9 +23,6 @@ public partial class UploadStepViewModel : SubmissionViewModel
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly EntryUploadHandlerFactory _entryUploadHandlerFactory;
|
||||
private readonly Progress<StreamProgress> _progress = new();
|
||||
private readonly ObservableAsPropertyHelper<bool> _progressIndeterminate;
|
||||
private readonly ObservableAsPropertyHelper<int> _progressPercentage;
|
||||
private readonly IRouter _router;
|
||||
private readonly IWindowService _windowService;
|
||||
private readonly IWorkshopClient _workshopClient;
|
||||
@@ -40,7 +37,7 @@ public partial class UploadStepViewModel : SubmissionViewModel
|
||||
IWorkshopClient workshopClient,
|
||||
IWorkshopService workshopService,
|
||||
EntryUploadHandlerFactory entryUploadHandlerFactory,
|
||||
IWindowService windowService,
|
||||
IWindowService windowService,
|
||||
IRouter router)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -54,19 +51,9 @@ public partial class UploadStepViewModel : SubmissionViewModel
|
||||
ContinueText = "Finish";
|
||||
Continue = ReactiveCommand.CreateFromTask(ExecuteContinue, this.WhenAnyValue(vm => vm.Finished));
|
||||
|
||||
_progressPercentage = Observable.FromEventPattern<StreamProgress>(x => _progress.ProgressChanged += x, x => _progress.ProgressChanged -= x)
|
||||
.Select(e => e.EventArgs.ProgressPercentage)
|
||||
.ToProperty(this, vm => vm.ProgressPercentage);
|
||||
_progressIndeterminate = Observable.FromEventPattern<StreamProgress>(x => _progress.ProgressChanged += x, x => _progress.ProgressChanged -= x)
|
||||
.Select(e => e.EventArgs.ProgressPercentage == 0)
|
||||
.ToProperty(this, vm => vm.ProgressIndeterminate);
|
||||
|
||||
this.WhenActivated(d => Observable.FromAsync(ExecuteUpload).Subscribe().DisposeWith(d));
|
||||
}
|
||||
|
||||
public int ProgressPercentage => _progressPercentage.Value;
|
||||
public bool ProgressIndeterminate => _progressIndeterminate.Value;
|
||||
|
||||
private async Task ExecuteUpload(CancellationToken cancellationToken)
|
||||
{
|
||||
// Use the existing entry or create a new one
|
||||
@@ -79,7 +66,7 @@ public partial class UploadStepViewModel : SubmissionViewModel
|
||||
try
|
||||
{
|
||||
IEntryUploadHandler uploadHandler = _entryUploadHandlerFactory.CreateHandler(State.EntryType);
|
||||
EntryUploadResult uploadResult = await uploadHandler.CreateReleaseAsync(_entryId.Value, State.EntrySource!, _progress, cancellationToken);
|
||||
EntryUploadResult uploadResult = await uploadHandler.CreateReleaseAsync(_entryId.Value, State.EntrySource!, cancellationToken);
|
||||
if (!uploadResult.IsSuccess)
|
||||
{
|
||||
string? message = uploadResult.Message;
|
||||
@@ -96,7 +83,7 @@ public partial class UploadStepViewModel : SubmissionViewModel
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to upload submission for entry {EntryId}", _entryId);
|
||||
|
||||
|
||||
// Something went wrong when creating a release :c
|
||||
// We'll keep the workshop entry so that the user can make changes and try again
|
||||
Failed = true;
|
||||
@@ -109,6 +96,8 @@ public partial class UploadStepViewModel : SubmissionViewModel
|
||||
|
||||
private async Task<long?> CreateEntry(CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
|
||||
IOperationResult<IAddEntryResult> result = await _workshopClient.AddEntry.ExecuteAsync(new CreateEntryInput
|
||||
{
|
||||
EntryType = State.EntryType,
|
||||
@@ -122,57 +111,40 @@ public partial class UploadStepViewModel : SubmissionViewModel
|
||||
long? entryId = result.Data?.AddEntry?.Id;
|
||||
if (result.IsErrorResult() || entryId == null)
|
||||
{
|
||||
await _windowService.ShowConfirmContentDialog("Failed to create workshop entry", string.Join("\r\n", result.Errors.Select(e => e.Message)), "Close", null);
|
||||
State.ChangeScreen<SubmitStepViewModel>();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await _windowService.ShowConfirmContentDialog("Failed to create workshop entry", string.Join("\r\n", result.Errors.Select(e => e.Message)), "Close", null);
|
||||
State.ChangeScreen<SubmitStepViewModel>();
|
||||
return null;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
foreach (ImageUploadRequest image in State.Images.ToList())
|
||||
{
|
||||
// Upload image
|
||||
try
|
||||
{
|
||||
ImageUploadResult imageUploadResult = await _workshopService.UploadEntryImage(entryId.Value, image, _progress, cancellationToken);
|
||||
if (!imageUploadResult.IsSuccess)
|
||||
throw new ArtemisWorkshopException(imageUploadResult.Message);
|
||||
State.Images.Remove(image);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// It's not critical if this fails
|
||||
await _windowService.ShowConfirmContentDialog("Failed to upload image", "Your submission will continue, you can try upload a new image afterwards\r\n" + e.Message, "Continue", null);
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
State.ChangeScreen<SubmitStepViewModel>();
|
||||
return null;
|
||||
}
|
||||
await TryImageUpload(async () => await _workshopService.UploadEntryImage(entryId.Value, image, cancellationToken));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
if (State.Icon == null)
|
||||
return entryId;
|
||||
|
||||
// Upload icon
|
||||
await TryImageUpload(async () => await _workshopService.SetEntryIcon(entryId.Value, State.Icon, cancellationToken));
|
||||
|
||||
return entryId;
|
||||
}
|
||||
|
||||
private async Task TryImageUpload(Func<Task<ImageUploadResult>> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
ImageUploadResult imageUploadResult = await _workshopService.SetEntryIcon(entryId.Value, _progress, State.Icon, cancellationToken);
|
||||
if (!imageUploadResult.IsSuccess)
|
||||
throw new ArtemisWorkshopException(imageUploadResult.Message);
|
||||
ImageUploadResult result = await action();
|
||||
if (!result.IsSuccess)
|
||||
throw new ArtemisWorkshopException(result.Message);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// It's not critical if this fails
|
||||
await _windowService.ShowConfirmContentDialog("Failed to upload icon", "Your submission will continue, you can try upload a new image afterwards\r\n" + e.Message, "Continue", null);
|
||||
await _windowService.ShowConfirmContentDialog("Failed to upload", "Your submission will continue, you can try upload a new image afterwards\r\n" + e.Message, "Continue", null);
|
||||
}
|
||||
|
||||
return entryId;
|
||||
}
|
||||
|
||||
private async Task ExecuteContinue()
|
||||
|
||||
Reference in new issue
Block a user