Workshop - Did all the things (Markdown stuff)

This commit is contained in:
Robert committed 2023-09-02 22:58:35 +02:00
1 parent c132edeb51
commit a0536b4302
47 files changed
+1017 -521

No files matched your search

@@ -3,9 +3,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:steps="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:categories="clr-namespace:Artemis.UI.Screens.Workshop.Categories"
xmlns:tagsInput="clr-namespace:Artemis.UI.Shared.TagsInput;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="970" d:DesignHeight="625"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.SpecificationsStepView"
x:DataType="steps:SpecificationsStepViewModel">
@@ -24,104 +21,8 @@
</TextBlock>
</StackPanel>
<ScrollViewer Grid.Row="1" Padding="0 0 20 0">
<StackPanel>
<StackPanel.Styles>
<Styles>
<Style Selector="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"></Setter>
</Style>
<Style Selector="Label">
<Setter Property="Margin" Value="0 8 0 0"></Setter>
</Style>
</Styles>
</StackPanel.Styles>
<Grid ColumnDefinitions="103,*">
<StackPanel Grid.Column="0" Width="95">
<Label Target="Name" Margin="0 15 0 0">Icon</Label>
<Button Width="95"
Height="95"
Command="{CompiledBinding SelectIcon}"
IsVisible="{CompiledBinding IconBitmap, Converter={x:Static ObjectConverters.IsNull}}">
<avalonia:MaterialIcon Kind="FolderOpen"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Width="30"
Height="30" />
</Button>
<Border IsVisible="{CompiledBinding IconBitmap, Converter={x:Static ObjectConverters.IsNotNull}}"
ClipToBounds="True"
CornerRadius="6"
VerticalAlignment="Center"
Width="95"
Height="95">
<Panel>
<Image Source="{CompiledBinding IconBitmap}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"></Image>
<Button Classes="icon-button image-picker"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Cursor="Hand"
Command="{CompiledBinding SelectIcon}"
ToolTip.Tip="Click to browse">
</Button>
</Panel>
</Border>
<TextBlock Foreground="{DynamicResource SystemFillColorCriticalBrush}" Margin="2 0"
IsVisible="{CompiledBinding !IconValid}"
TextWrapping="Wrap">
Icon required
</TextBlock>
</StackPanel>
<StackPanel Grid.Column="1">
<Label Target="Name" Margin="0 15 0 0">Name</Label>
<TextBox Name="Name" Text="{CompiledBinding Name}"></TextBox>
<Label Target="Summary" Margin="0 5 0 0">Summary</Label>
<TextBox Name="Summary" Text="{CompiledBinding Summary}"></TextBox>
</StackPanel>
</Grid>
<!-- <TextBlock Theme="{StaticResource CaptionTextBlockStyle}">A short summary of your submission's description</TextBlock> -->
<Label Margin="0 28 0 0">Categories</Label>
<ItemsControl ItemsSource="{CompiledBinding Categories}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="categories:CategoryViewModel">
<ToggleButton IsChecked="{CompiledBinding IsSelected}" Margin="0 0 5 5">
<StackPanel Orientation="Horizontal" Spacing="5">
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" />
<TextBlock Text="{CompiledBinding Name}" VerticalAlignment="Center" />
</StackPanel>
</ToggleButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Foreground="{DynamicResource SystemFillColorCriticalBrush}" Margin="2 0" IsVisible="{CompiledBinding !CategoriesValid}">
At least one category is required
</TextBlock>
<!-- <TextBlock Theme="{StaticResource CaptionTextBlockStyle}" Margin="0 -5 0 0">Pick one or more categories that suit your submission</TextBlock> -->
<Label>Tags</Label>
<tagsInput:TagsInput Tags="{CompiledBinding Tags}" />
<!-- <TextBlock Theme="{StaticResource CaptionTextBlockStyle}" Margin="0 -5 0 0">Tags are used by the search engine, use keywords that match your submission</TextBlock> -->
<Label Target="Description" Margin="0 28 0 0">Description</Label>
<TextBox AcceptsReturn="True" Name="Description" Text="{CompiledBinding Description}" Height="250"></TextBox>
<TextBlock Theme="{StaticResource CaptionTextBlockStyle}">Markdown supported, a better editor planned</TextBlock>
</StackPanel>
<ScrollViewer Grid.Row="1" Padding="0 0 20 0" Margin="0 20 0 0">
<ContentControl Content="{CompiledBinding EntrySpecificationsViewModel}"></ContentControl>
</ScrollViewer>
</Grid>
</UserControl>
@@ -1,93 +1,42 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.UI.Extensions;
using Artemis.UI.Screens.Workshop.Categories;
using Artemis.UI.Screens.Workshop.Entries;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Avalonia.Threading;
using DynamicData;
using DynamicData.Aggregation;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
using ReactiveUI.Validation.Helpers;
using StrawberryShake;
using Bitmap = Avalonia.Media.Imaging.Bitmap;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class SpecificationsStepViewModel : SubmissionViewModel
{
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<bool>? _categoriesValid;
private ObservableAsPropertyHelper<bool>? _iconValid;
private string _description = string.Empty;
private string _name = string.Empty;
private string _summary = string.Empty;
private Bitmap? _iconBitmap;
public SpecificationsStepViewModel(IWorkshopClient workshopClient, IWindowService windowService)
public SpecificationsStepViewModel(EntrySpecificationsViewModel entrySpecificationsViewModel)
{
_windowService = windowService;
EntrySpecificationsViewModel = entrySpecificationsViewModel;
GoBack = ReactiveCommand.Create(ExecuteGoBack);
Continue = ReactiveCommand.Create(ExecuteContinue, ValidationContext.Valid);
SelectIcon = ReactiveCommand.CreateFromTask(ExecuteSelectIcon);
Continue = ReactiveCommand.Create(ExecuteContinue, EntrySpecificationsViewModel.ValidationContext.Valid);
this.WhenActivated(d =>
this.WhenActivated((CompositeDisposable d) =>
{
DisplayName = $"{State.EntryType} Information";
// Load categories
Observable.FromAsync(workshopClient.GetCategories.ExecuteAsync).Subscribe(PopulateCategories).DisposeWith(d);
// Apply the state
ApplyFromState();
this.ClearValidationRules();
Disposable.Create(ClearIcon).DisposeWith(d);
EntrySpecificationsViewModel.ClearValidationRules();
});
}
public EntrySpecificationsViewModel EntrySpecificationsViewModel { get; }
public override ReactiveCommand<Unit, Unit> Continue { get; }
public override ReactiveCommand<Unit, Unit> GoBack { get; }
public ReactiveCommand<Unit, Unit> SelectIcon { get; }
public ObservableCollection<CategoryViewModel> Categories { get; } = new();
public ObservableCollection<string> Tags { get; } = new();
public bool CategoriesValid => _categoriesValid?.Value ?? true;
public bool IconValid => _iconValid?.Value ?? true;
public string Name
{
get => _name;
set => RaiseAndSetIfChanged(ref _name, value);
}
public string Summary
{
get => _summary;
set => RaiseAndSetIfChanged(ref _summary, value);
}
public string Description
{
get => _description;
set => RaiseAndSetIfChanged(ref _description, value);
}
public Bitmap? IconBitmap
{
get => _iconBitmap;
set => RaiseAndSetIfChanged(ref _iconBitmap, value);
}
private void ExecuteGoBack()
{
@@ -110,101 +59,61 @@ public class SpecificationsStepViewModel : SubmissionViewModel
private void ExecuteContinue()
{
if (!ValidationContext.Validations.Any())
if (!EntrySpecificationsViewModel.ValidationContext.Validations.Any())
{
// The ValidationContext seems to update asynchronously, so stop and schedule a retry
SetupDataValidation();
EntrySpecificationsViewModel.SetupDataValidation();
Dispatcher.UIThread.Post(ExecuteContinue);
return;
}
ApplyToState();
if (!ValidationContext.GetIsValid())
if (!EntrySpecificationsViewModel.ValidationContext.GetIsValid())
return;
State.ChangeScreen<SubmitStepViewModel>();
}
private async Task ExecuteSelectIcon()
{
string[]? result = await _windowService.CreateOpenFileDialog()
.HavingFilter(f => f.WithExtension("png").WithExtension("jpg").WithExtension("bmp").WithName("Bitmap image"))
.ShowAsync();
if (result == null)
return;
IconBitmap?.Dispose();
IconBitmap = BitmapExtensions.LoadAndResize(result[0], 128);
}
private void ClearIcon()
{
IconBitmap?.Dispose();
IconBitmap = null;
}
private void PopulateCategories(IOperationResult<IGetCategoriesResult> result)
{
Categories.Clear();
if (result.Data != null)
Categories.AddRange(result.Data.Categories.Select(c => new CategoryViewModel(c) {IsSelected = State.Categories.Contains(c.Id)}));
}
private void SetupDataValidation()
{
// Hopefully this can be avoided in the future
// https://github.com/reactiveui/ReactiveUI.Validation/discussions/558
this.ValidationRule(vm => vm.Name, s => !string.IsNullOrWhiteSpace(s), "Name is required");
this.ValidationRule(vm => vm.Summary, s => !string.IsNullOrWhiteSpace(s), "Summary is required");
this.ValidationRule(vm => vm.Description, s => !string.IsNullOrWhiteSpace(s), "Description is required");
// These don't use inputs that support validation messages, do so manually
ValidationHelper iconRule = this.ValidationRule(vm => vm.IconBitmap, s => s != null, "Icon required");
ValidationHelper categoriesRule = this.ValidationRule(vm => vm.Categories, Categories.ToObservableChangeSet().AutoRefresh(c => c.IsSelected).Filter(c => c.IsSelected).IsNotEmpty(),
"At least one category must be selected"
);
_iconValid = iconRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.IconValid);
_categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid);
}
private void ApplyFromState()
{
// Basic fields
Name = State.Name;
Summary = State.Summary;
Description = State.Description;
EntrySpecificationsViewModel.Name = State.Name;
EntrySpecificationsViewModel.Summary = State.Summary;
EntrySpecificationsViewModel.Description = State.Description;
// Tags
Tags.Clear();
Tags.AddRange(State.Tags);
EntrySpecificationsViewModel.Tags.Clear();
EntrySpecificationsViewModel.Tags.AddRange(State.Tags);
// Categories
EntrySpecificationsViewModel.PreselectedCategories = State.Categories;
// Icon
if (State.Icon != null)
{
State.Icon.Seek(0, SeekOrigin.Begin);
IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128);
EntrySpecificationsViewModel.IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128);
}
}
private void ApplyToState()
{
// Basic fields
State.Name = Name;
State.Summary = Summary;
State.Description = Description;
State.Name = EntrySpecificationsViewModel.Name;
State.Summary = EntrySpecificationsViewModel.Summary;
State.Description = EntrySpecificationsViewModel.Description;
// Categories and tasks
State.Categories = Categories.Where(c => c.IsSelected).Select(c => c.Id).ToList();
State.Tags = new List<string>(Tags);
State.Categories = EntrySpecificationsViewModel.Categories.Where(c => c.IsSelected).Select(c => c.Id).ToList();
State.Tags = new List<string>(EntrySpecificationsViewModel.Tags);
// Icon
State.Icon?.Dispose();
if (IconBitmap != null)
if (EntrySpecificationsViewModel.IconBitmap != null)
{
State.Icon = new MemoryStream();
IconBitmap.Save(State.Icon);
EntrySpecificationsViewModel.IconBitmap.Save(State.Icon);
}
else
{
@@ -152,13 +152,13 @@ public class UploadStepViewModel : SubmissionViewModel
switch (State.EntryType)
{
case EntryType.Layout:
await _router.Navigate($"workshop/layouts/{_entryId.Value}");
await _router.Navigate($"workshop/entries/layouts/{_entryId.Value}");
break;
case EntryType.Plugin:
await _router.Navigate($"workshop/plugins/{_entryId.Value}");
await _router.Navigate($"workshop/entries/plugins/{_entryId.Value}");
break;
case EntryType.Profile:
await _router.Navigate($"workshop/profiles/{_entryId.Value}");
await _router.Navigate($"workshop/entries/profiles/{_entryId.Value}");
break;
default:
throw new ArgumentOutOfRangeException();