1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Workshop - Simplify upload to a busy indicator, show images in profiles

This commit is contained in:
RobertBeekman 2023-12-10 22:44:03 +01:00
parent e33ae8a066
commit e304d67035
22 changed files with 125 additions and 242 deletions

View File

@ -47,6 +47,7 @@ public static class Constants
/// The full path to the Artemis logs folder /// The full path to the Artemis logs folder
/// </summary> /// </summary>
public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs"); public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs");
/// <summary> /// <summary>
/// The full path to the Artemis logs folder /// The full path to the Artemis logs folder
/// </summary> /// </summary>
@ -71,9 +72,9 @@ public static class Constants
/// <summary> /// <summary>
/// The current version of the application /// The current version of the application
/// </summary> /// </summary>
public static readonly string CurrentVersion = CoreAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion != "1.0.0" public static readonly string CurrentVersion = CoreAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion.StartsWith("1.0.0")
? CoreAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion ? "local"
: "local"; : CoreAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion;
/// <summary> /// <summary>
/// The plugin info used by core components of Artemis /// The plugin info used by core components of Artemis

View File

@ -30,6 +30,9 @@
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" /> <ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="AsyncImageLoader.Avalonia">
<HintPath>..\..\..\..\Users\Robert\.nuget\packages\asyncimageloader.avalonia\3.2.1\lib\netstandard2.1\AsyncImageLoader.Avalonia.dll</HintPath>
</Reference>
<Reference Include="RGB.NET.Layout"> <Reference Include="RGB.NET.Layout">
<HintPath>..\..\..\RGB.NET\bin\net7.0\RGB.NET.Layout.dll</HintPath> <HintPath>..\..\..\RGB.NET\bin\net7.0\RGB.NET.Layout.dll</HintPath>
</Reference> </Reference>

View File

@ -1,119 +0,0 @@
// Heavily based on:
// SkyClip
// - ProgressableStreamContent.cs
// --------------------------------------------------------------------
// Author: Jeff Hansen <jeff@jeffijoe.com>
// Copyright (C) Jeff Hansen 2015. All rights reserved.
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Utilities;
/// <summary>
/// Provides HTTP content based on a stream with support for IProgress.
/// </summary>
public class ProgressableStreamContent : StreamContent
{
private const int DEFAULT_BUFFER_SIZE = 4096;
private readonly int _bufferSize;
private readonly IProgress<StreamProgress> _progress;
private readonly Stream _streamToWrite;
private bool _contentConsumed;
/// <summary>
/// Initializes a new instance of the <see cref="ProgressableStreamContent" /> class.
/// </summary>
/// <param name="streamToWrite">The stream to write.</param>
/// <param name="progress">The downloader.</param>
public ProgressableStreamContent(Stream streamToWrite, IProgress<StreamProgress> progress) : this(streamToWrite, DEFAULT_BUFFER_SIZE, progress)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ProgressableStreamContent" /> class.
/// </summary>
/// <param name="streamToWrite">The stream to write.</param>
/// <param name="bufferSize">The buffer size.</param>
/// <param name="progress">The downloader.</param>
public ProgressableStreamContent(Stream streamToWrite, int bufferSize, IProgress<StreamProgress> progress) : base(streamToWrite, bufferSize)
{
if (bufferSize <= 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
_streamToWrite = streamToWrite;
_bufferSize = bufferSize;
_progress = progress;
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (disposing)
_streamToWrite.Dispose();
base.Dispose(disposing);
}
/// <inheritdoc />
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
{
await SerializeToStreamAsync(stream, context, CancellationToken.None);
}
/// <inheritdoc />
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken)
{
PrepareContent();
byte[] buffer = new byte[_bufferSize];
long size = _streamToWrite.Length;
int uploaded = 0;
await using (_streamToWrite)
{
while (!cancellationToken.IsCancellationRequested)
{
int length = await _streamToWrite.ReadAsync(buffer, cancellationToken);
if (length <= 0)
break;
uploaded += length;
_progress.Report(new StreamProgress(uploaded, size));
await stream.WriteAsync(buffer, 0, length, cancellationToken);
}
}
}
/// <inheritdoc />
protected override bool TryComputeLength(out long length)
{
length = _streamToWrite.Length;
return true;
}
/// <summary>
/// Prepares the content.
/// </summary>
/// <exception cref="System.InvalidOperationException">The stream has already been read.</exception>
private void PrepareContent()
{
if (_contentConsumed)
{
// If the content needs to be written to a target stream a 2nd time, then the stream must support
// seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target
// stream (e.g. a NetworkStream).
if (_streamToWrite.CanSeek)
_streamToWrite.Position = 0;
else
throw new InvalidOperationException("The stream has already been read.");
}
_contentConsumed = true;
}
}

File diff suppressed because one or more lines are too long

View File

@ -7,18 +7,25 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntryImageView" x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntryImageView"
x:DataType="details:EntryImageViewModel"> 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,*"> <Grid RowDefinitions="230,*">
<Rectangle Grid.Row="0" Fill="{DynamicResource CheckerboardBrush}" /> <Border Grid.Row="0" ClipToBounds="True" CornerRadius="4 4 0 0" Padding="0">
<Image Grid.Row="0" <Rectangle RenderOptions.BitmapInterpolationMode="HighQuality">
VerticalAlignment="Center" <Rectangle.Fill>
HorizontalAlignment="Center" <ImageBrush asyncImageLoader:ImageBrushLoader.Source="{CompiledBinding ThumbnailUrl}" Stretch="UniformToFill" />
RenderOptions.BitmapInterpolationMode="HighQuality" </Rectangle.Fill>
asyncImageLoader:ImageLoader.Source="{CompiledBinding ThumbnailUrl, Mode=OneWay}" /> </Rectangle>
<StackPanel Grid.Row="1" Margin="12"> </Border>
<TextBlock Text="{CompiledBinding Image.Name}" TextTrimming="CharacterEllipsis" /> <Border Grid.Row="1" ClipToBounds="True" CornerRadius="0 0 4 4" Background="{DynamicResource ControlFillColorDefaultBrush}">
<TextBlock Classes="subtitle" Text="{CompiledBinding Image.Description}" TextWrapping="Wrap" /> <StackPanel Margin="16">
</StackPanel> <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> </Grid>
</Border> </Border>
</UserControl> </UserControl>

View File

@ -1,8 +1,9 @@
using Artemis.WebClient.Workshop; using Artemis.UI.Shared;
using Artemis.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop.Entries.Details; namespace Artemis.UI.Screens.Workshop.Entries.Details;
public class EntryImageViewModel public class EntryImageViewModel : ViewModelBase
{ {
public EntryImageViewModel(IImage image) public EntryImageViewModel(IImage image)
{ {

View File

@ -8,21 +8,23 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntryImagesView" x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntryImagesView"
x:DataType="details:EntryImagesViewModel"> x:DataType="details:EntryImagesViewModel">
<ItemsControl ItemsSource="{CompiledBinding Images}" Margin="0 -16 0 0"> <ScrollViewer Classes="with-padding" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl.ItemsPanel> <ItemsControl ItemsSource="{CompiledBinding Images}" Width="300">
<ItemsPanelTemplate> <ItemsControl.Styles>
<VirtualizingStackPanel /> <Styles>
</ItemsPanelTemplate> <Style Selector="ItemsControl > ContentPresenter">
</ItemsControl.ItemsPanel> <Setter Property="Margin" Value="0 0 0 10"></Setter>
<ItemsControl.DataTemplates> </Style>
<DataTemplate x:DataType="details:EntryImageViewModel"> <Style Selector="ItemsControl > ContentPresenter:nth-last-child(1)">
<Border CornerRadius="6" <Setter Property="Margin" Value="0 0 0 0"></Setter>
Margin="0 16 0 0" </Style>
MaxWidth="300" </Styles>
ClipToBounds="True"> </ItemsControl.Styles>
<Image Stretch="UniformToFill" asyncImageLoader:ImageLoader.Source="{CompiledBinding ThumbnailUrl, Mode=OneWay}" /> <ItemsControl.ItemsPanel>
</Border> <ItemsPanelTemplate>
</DataTemplate> <VirtualizingStackPanel />
</ItemsControl.DataTemplates> </ItemsPanelTemplate>
</ItemsControl> </ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</UserControl> </UserControl>

View File

@ -23,7 +23,7 @@
</Styles> </Styles>
</controls:NavigationView.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> <controls:Frame.NavigationPageFactory>
<ui:PageFactory/> <ui:PageFactory/>
</controls:Frame.NavigationPageFactory> </controls:Frame.NavigationPageFactory>

View File

@ -1,10 +1,11 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Image; namespace Artemis.UI.Screens.Workshop.Image;
public partial class ImagePropertiesDialogView : UserControl public partial class ImagePropertiesDialogView : ReactiveUserControl<ImagePropertiesDialogViewModel>
{ {
public ImagePropertiesDialogView() public ImagePropertiesDialogView()
{ {

View File

@ -21,7 +21,7 @@
Source="{CompiledBinding Bitmap}" /> Source="{CompiledBinding Bitmap}" />
<StackPanel Grid.Row="1" Margin="12"> <StackPanel Grid.Row="1" Margin="12">
<TextBlock Text="{CompiledBinding Name, FallbackValue=Unnamed image}" TextTrimming="CharacterEllipsis" /> <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" /> <Separator Margin="-4 10" />
<TextBlock TextWrapping="Wrap" Classes="subtitle"> <TextBlock TextWrapping="Wrap" Classes="subtitle">
<Run Text="{CompiledBinding ImageDimensions}" /> - <Run Text="{CompiledBinding FileSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay}" /> <Run Text="{CompiledBinding ImageDimensions}" /> - <Run Text="{CompiledBinding FileSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay}" />

View File

@ -4,13 +4,14 @@ using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers; using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Threading; using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Validation.Extensions; using ReactiveUI.Validation.Extensions;
using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton;
namespace Artemis.UI.Screens.Workshop.Image; namespace Artemis.UI.Screens.Workshop.Image;
@ -30,30 +31,35 @@ public partial class ImageSubmissionViewModel : ValidatableViewModelBase
_image = image; _image = image;
_windowService = windowService; _windowService = windowService;
FileSize = _image.File.Length;
Name = _image.Name;
Description = _image.Description;
this.WhenActivated(d => this.WhenActivated(d =>
{ {
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() =>
{ {
_image.File.Seek(0, SeekOrigin.Begin); _image.File.Seek(0, SeekOrigin.Begin);
Bitmap = new Bitmap(_image.File); Bitmap = new Bitmap(_image.File);
FileSize = _image.File.Length;
ImageDimensions = Bitmap.Size.Width + "x" + Bitmap.Size.Height; ImageDimensions = Bitmap.Size.Width + "x" + Bitmap.Size.Height;
Name = _image.Name;
Description = _image.Description;
Bitmap.DisposeWith(d); Bitmap.DisposeWith(d);
}, DispatcherPriority.Background); }, DispatcherPriority.Background);
}); });
} }
public async Task Edit() public async Task<ContentDialogResult> Edit()
{ {
await _windowService.CreateContentDialog() ContentDialogResult result = await _windowService.CreateContentDialog()
.WithTitle("Edit image properties") .WithTitle("Edit image properties")
.WithViewModel(out ImagePropertiesDialogViewModel vm, _image) .WithViewModel(out ImagePropertiesDialogViewModel vm, _image)
.HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm)) .HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm))
.WithCloseButtonText("Cancel") .WithCloseButtonText("Cancel")
.WithDefaultButton(ContentDialogButton.Primary) .WithDefaultButton(ContentDialogButton.Primary)
.ShowAsync(); .ShowAsync();
Name = _image.Name;
Description = _image.Description;
return result;
} }
} }

View File

@ -158,7 +158,7 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
{ {
using MemoryStream stream = new(); using MemoryStream stream = new();
EntrySpecificationsViewModel.IconBitmap.Save(stream); 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) if (!imageResult.IsSuccess)
throw new ArtemisWorkshopException("Failed to upload image. " + imageResult.Message); throw new ArtemisWorkshopException("Failed to upload image. " + imageResult.Message);
} }

View File

@ -8,7 +8,7 @@
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView" x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView"
x:DataType="profile:ProfileDetailsViewModel"> x:DataType="profile:ProfileDetailsViewModel">
<Grid ColumnDefinitions="300,*, 300" RowDefinitions="Auto,*"> <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"> <Border Classes="card" VerticalAlignment="Top">
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" /> <ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
</Border> </Border>
@ -17,7 +17,7 @@
</Border> </Border>
</StackPanel> </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 Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
<mdxaml:MarkdownScrollViewer.Styles> <mdxaml:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml" /> <StyleInclude Source="/Styles/Markdown.axaml" />
@ -25,8 +25,6 @@
</mdxaml:MarkdownScrollViewer> </mdxaml:MarkdownScrollViewer>
</Border> </Border>
<StackPanel Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}"> <ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" />
<ContentControl Content="{CompiledBinding EntryImagesViewModel}" />
</StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -9,6 +9,7 @@ using Artemis.UI.Screens.Workshop.Image;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers; using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using DynamicData; using DynamicData;
using FluentAvalonia.UI.Controls;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
@ -49,7 +50,7 @@ public class ImagesStepViewModel : SubmissionViewModel
private ImageSubmissionViewModel CreateImageSubmissionViewModel(ImageUploadRequest image) private ImageSubmissionViewModel CreateImageSubmissionViewModel(ImageUploadRequest image)
{ {
ImageSubmissionViewModel viewModel = _getImageSubmissionViewModel(image); ImageSubmissionViewModel viewModel = _getImageSubmissionViewModel(image);
viewModel.Remove = ReactiveCommand.Create(() => _stateImages.Remove(image)); viewModel.Remove = ReactiveCommand.Create(() => RemoveImage(image));
return viewModel; return viewModel;
} }
@ -73,8 +74,23 @@ public class ImagesStepViewModel : SubmissionViewModel
} }
ImageUploadRequest request = new(stream, Path.GetFileName(path), string.Empty); ImageUploadRequest request = new(stream, Path.GetFileName(path), string.Empty);
_stateImages.Add(request); AddImage(request);
State.Images.Add(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);
}
} }

View File

@ -16,16 +16,10 @@
</Styles> </Styles>
</StackPanel.Styles> </StackPanel.Styles>
<TextBlock IsVisible="{CompiledBinding !Finished}" Theme="{StaticResource TitleTextBlockStyle}" TextAlignment="Center" TextWrapping="Wrap"> <StackPanel IsVisible="{CompiledBinding !Finished}">
Uploading your submission... <Lottie Path="/Assets/Animations/busy.json" Width="250" Height="250" Margin="0 100" ></Lottie>
</TextBlock> <TextBlock Theme="{StaticResource TitleTextBlockStyle}">Uploading your submission...</TextBlock>
</StackPanel>
<ProgressBar IsVisible="{CompiledBinding !Finished}"
Margin="0 15 0 0"
Width="380"
IsIndeterminate="{CompiledBinding ProgressIndeterminate}"
Value="{CompiledBinding ProgressPercentage}">
</ProgressBar>
<StackPanel IsVisible="{CompiledBinding Succeeded}"> <StackPanel IsVisible="{CompiledBinding Succeeded}">
<Lottie Path="/Assets/Animations/success.json" RepeatCount="1" Width="250" Height="250" Margin="0 100"></Lottie> <Lottie Path="/Assets/Animations/success.json" RepeatCount="1" Width="250" Height="250" Margin="0 100"></Lottie>

View File

@ -23,9 +23,6 @@ public partial class UploadStepViewModel : SubmissionViewModel
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly EntryUploadHandlerFactory _entryUploadHandlerFactory; 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 IRouter _router;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly IWorkshopClient _workshopClient; private readonly IWorkshopClient _workshopClient;
@ -54,19 +51,9 @@ public partial class UploadStepViewModel : SubmissionViewModel
ContinueText = "Finish"; ContinueText = "Finish";
Continue = ReactiveCommand.CreateFromTask(ExecuteContinue, this.WhenAnyValue(vm => vm.Finished)); 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)); 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) private async Task ExecuteUpload(CancellationToken cancellationToken)
{ {
// Use the existing entry or create a new one // Use the existing entry or create a new one
@ -79,7 +66,7 @@ public partial class UploadStepViewModel : SubmissionViewModel
try try
{ {
IEntryUploadHandler uploadHandler = _entryUploadHandlerFactory.CreateHandler(State.EntryType); 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) if (!uploadResult.IsSuccess)
{ {
string? message = uploadResult.Message; string? message = uploadResult.Message;
@ -109,6 +96,8 @@ public partial class UploadStepViewModel : SubmissionViewModel
private async Task<long?> CreateEntry(CancellationToken cancellationToken) private async Task<long?> CreateEntry(CancellationToken cancellationToken)
{ {
await Task.Delay(2000);
IOperationResult<IAddEntryResult> result = await _workshopClient.AddEntry.ExecuteAsync(new CreateEntryInput IOperationResult<IAddEntryResult> result = await _workshopClient.AddEntry.ExecuteAsync(new CreateEntryInput
{ {
EntryType = State.EntryType, EntryType = State.EntryType,
@ -122,57 +111,40 @@ public partial class UploadStepViewModel : SubmissionViewModel
long? entryId = result.Data?.AddEntry?.Id; long? entryId = result.Data?.AddEntry?.Id;
if (result.IsErrorResult() || entryId == null) 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); 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)
{
State.ChangeScreen<SubmitStepViewModel>(); State.ChangeScreen<SubmitStepViewModel>();
return null; return null;
} }
cancellationToken.ThrowIfCancellationRequested();
foreach (ImageUploadRequest image in State.Images.ToList()) foreach (ImageUploadRequest image in State.Images.ToList())
{ {
// Upload image await TryImageUpload(async () => await _workshopService.UploadEntryImage(entryId.Value, image, cancellationToken));
try cancellationToken.ThrowIfCancellationRequested();
{
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;
}
} }
if (State.Icon == null) if (State.Icon == null)
return entryId; return entryId;
// Upload icon // Upload icon
await TryImageUpload(async () => await _workshopService.SetEntryIcon(entryId.Value, State.Icon, cancellationToken));
return entryId;
}
private async Task TryImageUpload(Func<Task<ImageUploadResult>> action)
{
try try
{ {
ImageUploadResult imageUploadResult = await _workshopService.SetEntryIcon(entryId.Value, _progress, State.Icon, cancellationToken); ImageUploadResult result = await action();
if (!imageUploadResult.IsSuccess) if (!result.IsSuccess)
throw new ArtemisWorkshopException(imageUploadResult.Message); throw new ArtemisWorkshopException(result.Message);
} }
catch (Exception e) catch (Exception e)
{ {
// It's not critical if this fails // 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() private async Task ExecuteContinue()

View File

@ -4,5 +4,5 @@ namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
public interface IEntryUploadHandler public interface IEntryUploadHandler
{ {
Task<EntryUploadResult> CreateReleaseAsync(long entryId, IEntrySource entrySource, Progress<StreamProgress> progress, CancellationToken cancellationToken); Task<EntryUploadResult> CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken);
} }

View File

@ -19,7 +19,7 @@ public class LayoutEntryUploadHandler : IEntryUploadHandler
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<EntryUploadResult> CreateReleaseAsync(long entryId, IEntrySource entrySource, Progress<StreamProgress> progress, CancellationToken cancellationToken) public async Task<EntryUploadResult> CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken)
{ {
if (entrySource is not LayoutEntrySource source) if (entrySource is not LayoutEntrySource source)
throw new InvalidOperationException("Can only create releases for layouts"); throw new InvalidOperationException("Can only create releases for layouts");
@ -67,7 +67,7 @@ public class LayoutEntryUploadHandler : IEntryUploadHandler
// Construct the request // Construct the request
MultipartFormDataContent content = new(); MultipartFormDataContent content = new();
ProgressableStreamContent streamContent = new(archiveStream, progress); StreamContent streamContent = new(archiveStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
content.Add(streamContent, "file", "file.zip"); content.Add(streamContent, "file", "file.zip");

View File

@ -18,7 +18,7 @@ public class ProfileEntryUploadHandler : IEntryUploadHandler
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<EntryUploadResult> CreateReleaseAsync(long entryId, IEntrySource entrySource, Progress<StreamProgress> progress, CancellationToken cancellationToken) public async Task<EntryUploadResult> CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken)
{ {
if (entrySource is not ProfileEntrySource source) if (entrySource is not ProfileEntrySource source)
throw new InvalidOperationException("Can only create releases for profile configurations"); throw new InvalidOperationException("Can only create releases for profile configurations");
@ -30,7 +30,7 @@ public class ProfileEntryUploadHandler : IEntryUploadHandler
// Construct the request // Construct the request
MultipartFormDataContent content = new(); MultipartFormDataContent content = new();
ProgressableStreamContent streamContent = new(archiveStream, progress); StreamContent streamContent = new(archiveStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
content.Add(streamContent, "file", "file.zip"); content.Add(streamContent, "file", "file.zip");

View File

@ -6,8 +6,8 @@ namespace Artemis.WebClient.Workshop.Services;
public interface IWorkshopService public interface IWorkshopService
{ {
Task<Stream?> GetEntryIcon(long entryId, CancellationToken cancellationToken); Task<Stream?> GetEntryIcon(long entryId, CancellationToken cancellationToken);
Task<ImageUploadResult> SetEntryIcon(long entryId, Progress<StreamProgress> progress, Stream icon, CancellationToken cancellationToken); Task<ImageUploadResult> SetEntryIcon(long entryId, Stream icon, CancellationToken cancellationToken);
Task<ImageUploadResult> UploadEntryImage(long entryId, ImageUploadRequest request, Progress<StreamProgress> progress, CancellationToken cancellationToken); Task<ImageUploadResult> UploadEntryImage(long entryId, ImageUploadRequest request, CancellationToken cancellationToken);
Task<WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken); Task<WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken);
Task<bool> ValidateWorkshopStatus(CancellationToken cancellationToken); Task<bool> ValidateWorkshopStatus(CancellationToken cancellationToken);
Task NavigateToEntry(long entryId, EntryType entryType); Task NavigateToEntry(long entryId, EntryType entryType);

View File

@ -36,7 +36,7 @@ public class WorkshopService : IWorkshopService
} }
} }
public async Task<ImageUploadResult> SetEntryIcon(long entryId, Progress<StreamProgress> progress, Stream icon, CancellationToken cancellationToken) public async Task<ImageUploadResult> SetEntryIcon(long entryId, Stream icon, CancellationToken cancellationToken)
{ {
icon.Seek(0, SeekOrigin.Begin); icon.Seek(0, SeekOrigin.Begin);
@ -45,7 +45,7 @@ public class WorkshopService : IWorkshopService
// Construct the request // Construct the request
MultipartFormDataContent content = new(); MultipartFormDataContent content = new();
ProgressableStreamContent streamContent = new(icon, progress); StreamContent streamContent = new(icon);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png"); streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
content.Add(streamContent, "file", "file.png"); content.Add(streamContent, "file", "file.png");
@ -57,7 +57,7 @@ public class WorkshopService : IWorkshopService
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<ImageUploadResult> UploadEntryImage(long entryId, ImageUploadRequest request, Progress<StreamProgress> progress, CancellationToken cancellationToken) public async Task<ImageUploadResult> UploadEntryImage(long entryId, ImageUploadRequest request, CancellationToken cancellationToken)
{ {
request.File.Seek(0, SeekOrigin.Begin); request.File.Seek(0, SeekOrigin.Begin);
@ -66,7 +66,7 @@ public class WorkshopService : IWorkshopService
// Construct the request // Construct the request
MultipartFormDataContent content = new(); MultipartFormDataContent content = new();
ProgressableStreamContent streamContent = new(request.File, progress); StreamContent streamContent = new(request.File);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png"); streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
content.Add(streamContent, "file", "file.png"); content.Add(streamContent, "file", "file.png");
content.Add(new StringContent(request.Name), "Name"); content.Add(new StringContent(request.Name), "Name");