1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 13:28:33 +00:00

UI - Tweaked monospace font sizing

This commit is contained in:
Robert 2023-09-05 21:39:43 +02:00
parent 2ee170b803
commit fcde1d4ecc
25 changed files with 281 additions and 214 deletions

View File

@ -67,6 +67,7 @@
<TextBlock Grid.Column="1"
Text="{CompiledBinding DisplayValue}"
FontFamily="{StaticResource RobotoMono}"
FontSize="13"
HorizontalAlignment="Right"
Margin="0 0 10 0" />
</Grid>
@ -81,7 +82,7 @@
IsVisible="{CompiledBinding IsEventPicker, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type dataModelPicker:DataModelPicker}}}"/>
</StackPanel>
<ContentControl Grid.Column="1" Content="{CompiledBinding DisplayViewModel}" FontFamily="{StaticResource RobotoMono}" Margin="0 0 10 0" />
<ContentControl Grid.Column="1" Content="{CompiledBinding DisplayViewModel}" FontFamily="{StaticResource RobotoMono}" FontSize="13" Margin="0 0 10 0" />
</Grid>
</TreeDataTemplate>
@ -91,6 +92,7 @@
<TextBlock Grid.Column="1"
Text="{CompiledBinding CountDisplay, Mode=OneWay}"
FontFamily="{StaticResource RobotoMono}"
FontSize="13"
HorizontalAlignment="Right"
Margin="0 0 10 0" />
</Grid>

View File

@ -52,8 +52,8 @@
<TextBlock Grid.Column="1" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
<TextBlock Grid.Column="2"
Text="{CompiledBinding DisplayValue}"
FontSize="13"
FontFamily="{StaticResource RobotoMono}"
FontSize="13"
HorizontalAlignment="Right" />
</Grid>

View File

@ -10,8 +10,8 @@
<SelectableTextBlock
Inlines="{CompiledBinding Lines}"
FontFamily="{StaticResource RobotoMono}"
FontSize="12"
SizeChanged="Control_OnSizeChanged"
SelectionBrush="{StaticResource TextControlSelectionHighlightColor}"
/>
SelectionBrush="{StaticResource TextControlSelectionHighlightColor}"/>
</ScrollViewer>
</UserControl>

View File

@ -20,6 +20,7 @@
<SelectableTextBlock
Inlines="{CompiledBinding Lines}"
FontFamily="{StaticResource RobotoMono}"
FontSize="12"
SizeChanged="Control_OnSizeChanged"
SelectionBrush="{StaticResource TextControlSelectionHighlightColor}" />
</ScrollViewer>

View File

@ -41,7 +41,8 @@
Text="{CompiledBinding Converter={StaticResource SKColorToStringConverter}, Mode=OneWay}"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
FontFamily="{StaticResource RobotoMono}" />
FontFamily="{StaticResource RobotoMono}"
FontSize="13"/>
<Border Margin="5 0 0 0"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
@ -61,16 +62,16 @@
</StackPanel>
</DataTemplate>
<DataTemplate DataType="core:ColorGradient">
<TextBlock Text="Color gradient" FontFamily="{StaticResource RobotoMono}" />
<TextBlock Text="Color gradient" FontFamily="{StaticResource RobotoMono}" FontSize="13"/>
</DataTemplate>
<DataTemplate DataType="core:Numeric">
<TextBlock Text="{CompiledBinding Mode=OneWay}" FontFamily="{StaticResource RobotoMono}" />
<TextBlock Text="{CompiledBinding Mode=OneWay}" FontFamily="{StaticResource RobotoMono}" FontSize="13"/>
</DataTemplate>
<DataTemplate DataType="collections:IList">
<TextBlock Text="{CompiledBinding Count, StringFormat='List - {0} item(s)', Mode=OneWay}" FontFamily="{StaticResource RobotoMono}" />
<TextBlock Text="{CompiledBinding Count, StringFormat='List - {0} item(s)', Mode=OneWay}" FontFamily="{StaticResource RobotoMono}" FontSize="13"/>
</DataTemplate>
<DataTemplate DataType="system:Object">
<TextBlock Text="{CompiledBinding Mode=OneWay}" FontFamily="{StaticResource RobotoMono}" />
<TextBlock Text="{CompiledBinding Mode=OneWay}" FontFamily="{StaticResource RobotoMono}" FontSize="13"/>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>

View File

@ -12,7 +12,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Workshop.Entries.EntrySpecificationsView"
x:DataType="entries:EntrySpecificationsViewModel">
<Grid RowDefinitions="Auto,*,Auto">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<StackPanel>
<StackPanel.Styles>
<Styles>
@ -25,8 +25,8 @@
</Styles>
</StackPanel.Styles>
<Grid ColumnDefinitions="103,*">
<StackPanel Grid.Column="0" Width="95">
<Grid ColumnDefinitions="105,*">
<StackPanel Grid.Column="0" Width="95" HorizontalAlignment="Left">
<Label Target="Name" Margin="0">Icon</Label>
<Button Width="95"
Height="95"
@ -92,12 +92,22 @@
<Label>Tags</Label>
<tagsInput:TagsInput Tags="{CompiledBinding Tags}" />
<Label Target="DescriptionEditor" Margin="0 28 0 0">Description</Label>
</StackPanel>
<Grid Row="1" ColumnDefinitions="Auto,*">
<Label Grid.Column="0" Target="DescriptionEditor" Margin="0 28 0 0">Description</Label>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<CheckBox Name="SynchronizedScrolling" IsChecked="True" VerticalAlignment="Bottom">Synchronized scrolling</CheckBox>
<controls:HyperlinkButton
Margin="0 0 0 -20"
Content="Markdown supported"
NavigateUri="https://wiki.artemis-rgb.com/guides/user/markdown?mtm_campaign=artemis&amp;mtm_kwd=markdown-editor"
HorizontalAlignment="Right"/>
</StackPanel>
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,*">
<Grid Grid.Row="2" ColumnDefinitions="*,Auto,*">
<Border Grid.Column="0" BorderThickness="1"
BorderBrush="{DynamicResource TextControlBorderBrush}"
CornerRadius="{DynamicResource ControlCornerRadius}"
@ -105,6 +115,7 @@
Padding="{DynamicResource TextControlThemePadding}">
<avaloniaEdit:TextEditor
FontFamily="{StaticResource RobotoMono}"
FontSize="13"
Name="DescriptionEditor"
Document="{CompiledBinding MarkdownDocument}"
WordWrap="True" />
@ -123,17 +134,12 @@
</mdxaml:MarkdownScrollViewer>
</Border>
</Grid>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
<CheckBox Name="SynchronizedScrolling" IsChecked="True" VerticalAlignment="Bottom">Synchronized scrolling</CheckBox>
<controls:HyperlinkButton
Margin="0 5 0 0"
Content="Learn more about Markdown on the wiki"
NavigateUri="https://wiki.artemis-rgb.com/guides/user/markdown?mtm_campaign=artemis&amp;mtm_kwd=markdown-editor"
HorizontalAlignment="Right"
VerticalAlignment="Top" />
</StackPanel>
<TextBlock Grid.Row="3"
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
Margin="2 8 0 0"
IsVisible="{CompiledBinding !DescriptionValid}">
A description is required
</TextBlock>
</Grid>
</UserControl>

View File

@ -11,6 +11,7 @@ using Artemis.UI.Screens.Workshop.Categories;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Avalonia.Media.Imaging;
using AvaloniaEdit.Document;
using DynamicData;
using DynamicData.Aggregation;
@ -19,32 +20,55 @@ using ReactiveUI;
using ReactiveUI.Validation.Extensions;
using ReactiveUI.Validation.Helpers;
using StrawberryShake;
using Bitmap = Avalonia.Media.Imaging.Bitmap;
namespace Artemis.UI.Screens.Workshop.Entries;
public class EntrySpecificationsViewModel : ValidatableViewModelBase
{
private readonly ObservableAsPropertyHelper<bool> _categoriesValid;
private readonly ObservableAsPropertyHelper<bool> _iconValid;
private readonly ObservableAsPropertyHelper<bool> _descriptionValid;
private readonly IWorkshopClient _workshopClient;
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;
private TextDocument? _markdownDocument;
private string _name = string.Empty;
private string _summary = string.Empty;
private bool _iconChanged;
public EntrySpecificationsViewModel(IWorkshopClient workshopClient, IWindowService windowService)
{
_workshopClient = workshopClient;
_windowService = windowService;
SelectIcon = ReactiveCommand.CreateFromTask(ExecuteSelectIcon);
Categories.ToObservableChangeSet()
.AutoRefresh(c => c.IsSelected)
.Filter(c => c.IsSelected)
.Transform(c => c.Id)
.Bind(out ReadOnlyObservableCollection<int> selectedCategories)
.Subscribe();
SelectedCategories = selectedCategories;
this.WhenActivated(d =>
this.ValidationRule(vm => vm.Name, s => !string.IsNullOrWhiteSpace(s), "Name is required");
this.ValidationRule(vm => vm.Summary, s => !string.IsNullOrWhiteSpace(s), "Summary is required");
ValidationHelper descriptionRule = 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);
_descriptionValid = descriptionRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.DescriptionValid);
this.WhenActivatedAsync(async d =>
{
// Load categories
Observable.FromAsync(workshopClient.GetCategories.ExecuteAsync).Subscribe(PopulateCategories).DisposeWith(d);
this.ClearValidationRules();
await PopulateCategories();
MarkdownDocument = new TextDocument(new StringTextSource(Description));
MarkdownDocument.TextChanged += MarkdownDocumentOnTextChanged;
@ -57,17 +81,15 @@ public class EntrySpecificationsViewModel : ValidatableViewModelBase
});
}
private void MarkdownDocumentOnTextChanged(object? sender, EventArgs e)
{
Description = MarkdownDocument?.Text ?? string.Empty;
}
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 ReadOnlyObservableCollection<int> SelectedCategories { get; }
public bool CategoriesValid => _categoriesValid.Value ;
public bool IconValid => _iconValid.Value;
public bool DescriptionValid => _descriptionValid.Value;
public string Name
{
@ -99,23 +121,17 @@ public class EntrySpecificationsViewModel : ValidatableViewModelBase
set => RaiseAndSetIfChanged(ref _markdownDocument, value);
}
public List<int> PreselectedCategories { get; set; } = new List<int>();
public void SetupDataValidation()
public bool IconChanged
{
// 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");
get => _iconChanged;
private set => RaiseAndSetIfChanged(ref _iconChanged, value);
}
// 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);
public List<int> PreselectedCategories { get; set; } = new();
private void MarkdownDocumentOnTextChanged(object? sender, EventArgs e)
{
Description = MarkdownDocument?.Text ?? string.Empty;
}
private async Task ExecuteSelectIcon()
@ -129,6 +145,7 @@ public class EntrySpecificationsViewModel : ValidatableViewModelBase
IconBitmap?.Dispose();
IconBitmap = BitmapExtensions.LoadAndResize(result[0], 128);
IconChanged = true;
}
private void ClearIcon()
@ -137,10 +154,11 @@ public class EntrySpecificationsViewModel : ValidatableViewModelBase
IconBitmap = null;
}
private void PopulateCategories(IOperationResult<IGetCategoriesResult> result)
private async Task PopulateCategories()
{
IOperationResult<IGetCategoriesResult> categories = await _workshopClient.GetCategories.ExecuteAsync();
Categories.Clear();
if (result.Data != null)
Categories.AddRange(result.Data.Categories.Select(c => new CategoryViewModel(c) {IsSelected = PreselectedCategories.Contains(c.Id)}));
if (categories.Data != null)
Categories.AddRange(categories.Data.Categories.Select(c => new CategoryViewModel(c) {IsSelected = PreselectedCategories.Contains(c.Id)}));
}
}

View File

@ -12,8 +12,8 @@
<UserControl.Resources>
<converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources>
<Grid ColumnDefinitions="300,*">
<StackPanel Grid.Column="0" Spacing="10">
<Grid ColumnDefinitions="300,*" RowDefinitions="*, Auto">
<StackPanel Grid.Column="0" Grid.RowSpan="2" Spacing="10">
<Border Classes="card" VerticalAlignment="Top" Margin="0 0 10 0">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Management</TextBlock>
@ -48,7 +48,11 @@
View workshop page
</controls:HyperlinkButton>
</StackPanel>
<ContentControl Grid.Column="1" Content="{CompiledBinding EntrySpecificationsViewModel}"></ContentControl>
<ContentControl Grid.Column="1" Grid.Row="0" Content="{CompiledBinding EntrySpecificationsViewModel}"></ContentControl>
<StackPanel Grid.Column="1" Grid.Row="1" HorizontalAlignment="Right" Spacing="5" Orientation="Horizontal" Margin="0 10 0 0">
<Button Command="{CompiledBinding DiscardChanges}">Discard changes</Button>
<Button Command="{CompiledBinding SaveChanges}">Save</Button>
</StackPanel>
</Grid>
</UserControl>

View File

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
@ -10,8 +12,11 @@ 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;
@ -21,37 +26,33 @@ namespace Artemis.UI.Screens.Workshop.Library;
public class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailParameters>
{
private readonly IWorkshopClient _client;
private readonly IHttpClientFactory _httpClientFactory;
private readonly Func<EntrySpecificationsViewModel> _getEntrySpecificationsViewModel;
private readonly Func<EntrySpecificationsViewModel> _getGetSpecificationsVm;
private readonly IRouter _router;
private readonly IWindowService _windowService;
private readonly IWorkshopService _workshopService;
private readonly IRouter _router;
private IGetSubmittedEntryById_Entry? _entry;
private EntrySpecificationsViewModel? _entrySpecificationsViewModel;
private bool _hasChanges;
public SubmissionDetailViewModel(IWorkshopClient client,
IHttpClientFactory httpClientFactory,
IWindowService windowService,
IWorkshopService workshopService,
IRouter router,
Func<EntrySpecificationsViewModel> entrySpecificationsViewModel)
{
public SubmissionDetailViewModel(IWorkshopClient client, IWindowService windowService, IWorkshopService workshopService, IRouter router, Func<EntrySpecificationsViewModel> getSpecificationsVm) {
_client = client;
_httpClientFactory = httpClientFactory;
_windowService = windowService;
_workshopService = workshopService;
_router = router;
_getEntrySpecificationsViewModel = entrySpecificationsViewModel;
_getGetSpecificationsVm = getSpecificationsVm;
Save = ReactiveCommand.CreateFromTask(ExecuteSave);
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<Unit, Unit> Save { get; }
public ReactiveCommand<Unit, Unit> CreateRelease { get; }
public ReactiveCommand<Unit, Unit> DeleteSubmission { get; }
public ReactiveCommand<Unit, Unit> ViewWorkshopPage { get; }
public ReactiveCommand<Unit, Unit> SaveChanges { get; }
public ReactiveCommand<Unit, Unit> DiscardChanges { get; }
public EntrySpecificationsViewModel? EntrySpecificationsViewModel
{
@ -65,8 +66,12 @@ public class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailParameters
set => RaiseAndSetIfChanged(ref _entry, value);
}
public ReactiveCommand<Unit, Unit> ViewWorkshopPage { get; }
public bool HasChanges
{
get => _hasChanges;
private set => RaiseAndSetIfChanged(ref _hasChanges, value);
}
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
IOperationResult<IGetSubmittedEntryByIdResult> result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken);
@ -77,12 +82,29 @@ public class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailParameters
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;
EntrySpecificationsViewModel viewModel = _getEntrySpecificationsViewModel();
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;
@ -95,6 +117,9 @@ public class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailParameters
viewModel.Tags.Add(tag);
EntrySpecificationsViewModel = viewModel;
EntrySpecificationsViewModel.PropertyChanged += EntrySpecificationsViewModelOnPropertyChanged;
((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged += SelectedCategoriesOnCollectionChanged;
EntrySpecificationsViewModel.Tags.CollectionChanged += TagsOnCollectionChanged;
}
private async Task<Bitmap?> GetEntryIcon(CancellationToken cancellationToken)
@ -102,26 +127,59 @@ public class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailParameters
if (Entry == null)
return null;
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME);
try
{
HttpResponseMessage response = await client.GetAsync($"entries/{Entry.Id}/icon", cancellationToken);
response.EnsureSuccessStatusCode();
Stream data = await response.Content.ReadAsStreamAsync(cancellationToken);
return new Bitmap(data);
}
catch (HttpRequestException)
{
// ignored
return null;
}
Stream? stream = await _workshopService.GetEntryIcon(Entry.Id, cancellationToken);
return stream != null ? new Bitmap(stream) : null;
}
private async Task ExecuteSave(CancellationToken cancellationToken)
private void UpdateHasChanges()
{
UpdateEntryInput input = new();
if (EntrySpecificationsViewModel == null || Entry == null)
return;
List<int> 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.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<IUpdateEntryResult> 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<StreamProgress>(), stream, cancellationToken);
if (!imageResult.IsSuccess)
throw new ArtemisWorkshopException("Failed to upload image. " + imageResult.Message);
}
HasChanges = false;
}
private async Task ExecuteCreateRelease(CancellationToken cancellationToken)
@ -152,4 +210,19 @@ public class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailParameters
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();
}
}

View File

@ -1,6 +1,4 @@
using System.Reactive;
using System.Reactive.Linq;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
using Artemis.WebClient.Workshop;
using ReactiveUI;
@ -23,12 +21,6 @@ public class EntryTypeStepViewModel : SubmissionViewModel
set => RaiseAndSetIfChanged(ref _selectedEntryType, value);
}
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> Continue { get; }
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> GoBack { get; }
private void ExecuteContinue()
{
if (SelectedEntryType == null)

View File

@ -1,6 +1,4 @@
using System;
using System.Linq;
using System.Reactive;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
@ -29,12 +27,6 @@ public class LoginStepViewModel : SubmissionViewModel
ContinueText = "Log In";
}
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> Continue { get; }
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> GoBack { get; } = null!;
private async Task ExecuteLogin(CancellationToken ct)
{
ContentDialogResult result = await _windowService.CreateContentDialog().WithViewModel(out WorkshopLoginViewModel _).WithTitle("Workshop login").ShowAsync();

View File

@ -6,31 +6,30 @@ using Artemis.Core.Services;
using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using FluentAvalonia.Core;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
public class ProfileAdaptionHintsLayerViewModel : ViewModelBase
{
private readonly IWindowService _windowService;
private readonly IProfileService _profileService;
private readonly ObservableAsPropertyHelper<string> _adaptionHintText;
private readonly IProfileService _profileService;
private readonly IWindowService _windowService;
private int _adaptionHintCount;
public Layer Layer { get; }
public ProfileAdaptionHintsLayerViewModel(Layer layer, IWindowService windowService, IProfileService profileService)
{
_windowService = windowService;
_profileService = profileService;
_adaptionHintText = this.WhenAnyValue(vm => vm.AdaptionHintCount).Select(c => c == 1 ? "1 adaption hint" : $"{c} adaption hints").ToProperty(this, vm => vm.AdaptionHintText);
Layer = layer;
EditAdaptionHints = ReactiveCommand.CreateFromTask(ExecuteEditAdaptionHints);
AdaptionHintCount = layer.Adapter.AdaptionHints.Count;
}
public Layer Layer { get; }
public ReactiveCommand<Unit, Unit> EditAdaptionHints { get; }
public int AdaptionHintCount

View File

@ -3,23 +3,22 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs;
using Artemis.UI.Shared.Services;
using DynamicData;
using ReactiveUI;
using DynamicData.Aggregation;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
public class ProfileAdaptionHintsStepViewModel : SubmissionViewModel
{
private readonly IWindowService _windowService;
private readonly IProfileService _profileService;
private readonly SourceList<ProfileAdaptionHintsLayerViewModel> _layers;
private readonly IProfileService _profileService;
private readonly IWindowService _windowService;
public ProfileAdaptionHintsStepViewModel(IWindowService windowService, IProfileService profileService, Func<Layer, ProfileAdaptionHintsLayerViewModel> getLayerViewModel)
{
@ -36,18 +35,14 @@ public class ProfileAdaptionHintsStepViewModel : SubmissionViewModel
this.WhenActivated((CompositeDisposable _) =>
{
if (State.EntrySource is ProfileConfiguration profileConfiguration && profileConfiguration.Profile != null)
{
_layers.Edit(l =>
{
l.Clear();
l.AddRange(profileConfiguration.Profile.GetAllLayers().Select(getLayerViewModel));
});
}
});
}
public override ReactiveCommand<Unit, Unit> Continue { get; }
public override ReactiveCommand<Unit, Unit> GoBack { get; }
public ReactiveCommand<Layer, Unit> EditAdaptionHints { get; }
public ReadOnlyObservableCollection<ProfileAdaptionHintsLayerViewModel> Layers { get; }
@ -61,7 +56,7 @@ public class ProfileAdaptionHintsStepViewModel : SubmissionViewModel
{
if (Layers.Any(l => l.AdaptionHintCount == 0))
return;
if (State.EntryId == null)
State.ChangeScreen<SpecificationsStepViewModel>();
else

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
@ -52,12 +51,6 @@ public class ProfileSelectionStepViewModel : SubmissionViewModel
set => RaiseAndSetIfChanged(ref _selectedProfile, value);
}
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> Continue { get; }
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> GoBack { get; }
private void Update(ProfileConfiguration? profileConfiguration)
{
ProfilePreview.ProfileConfiguration = null;

View File

@ -1,5 +1,3 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;

View File

@ -2,41 +2,38 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using Artemis.UI.Extensions;
using Artemis.UI.Screens.Workshop.Entries;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
using Artemis.WebClient.Workshop;
using Avalonia.Threading;
using DynamicData;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class SpecificationsStepViewModel : SubmissionViewModel
{
public SpecificationsStepViewModel(EntrySpecificationsViewModel entrySpecificationsViewModel)
{
EntrySpecificationsViewModel = entrySpecificationsViewModel;
GoBack = ReactiveCommand.Create(ExecuteGoBack);
Continue = ReactiveCommand.Create(ExecuteContinue, EntrySpecificationsViewModel.ValidationContext.Valid);
private readonly Func<EntrySpecificationsViewModel> _getEntrySpecificationsViewModel;
private EntrySpecificationsViewModel? _entrySpecificationsViewModel;
public SpecificationsStepViewModel(Func<EntrySpecificationsViewModel> getEntrySpecificationsViewModel)
{
_getEntrySpecificationsViewModel = getEntrySpecificationsViewModel;
GoBack = ReactiveCommand.Create(ExecuteGoBack);
this.WhenActivated((CompositeDisposable d) =>
{
DisplayName = $"{State.EntryType} Information";
// Apply the state
ApplyFromState();
EntrySpecificationsViewModel.ClearValidationRules();
});
}
public EntrySpecificationsViewModel EntrySpecificationsViewModel { get; }
public override ReactiveCommand<Unit, Unit> Continue { get; }
public override ReactiveCommand<Unit, Unit> GoBack { get; }
public EntrySpecificationsViewModel? EntrySpecificationsViewModel
{
get => _entrySpecificationsViewModel;
set => RaiseAndSetIfChanged(ref _entrySpecificationsViewModel, value);
}
private void ExecuteGoBack()
{
@ -59,46 +56,45 @@ public class SpecificationsStepViewModel : SubmissionViewModel
private void ExecuteContinue()
{
if (!EntrySpecificationsViewModel.ValidationContext.Validations.Any())
{
// The ValidationContext seems to update asynchronously, so stop and schedule a retry
EntrySpecificationsViewModel.SetupDataValidation();
Dispatcher.UIThread.Post(ExecuteContinue);
if (EntrySpecificationsViewModel == null || !EntrySpecificationsViewModel.ValidationContext.GetIsValid())
return;
}
ApplyToState();
if (!EntrySpecificationsViewModel.ValidationContext.GetIsValid())
return;
State.ChangeScreen<SubmitStepViewModel>();
}
private void ApplyFromState()
{
EntrySpecificationsViewModel viewModel = _getEntrySpecificationsViewModel();
// Basic fields
EntrySpecificationsViewModel.Name = State.Name;
EntrySpecificationsViewModel.Summary = State.Summary;
EntrySpecificationsViewModel.Description = State.Description;
viewModel.Name = State.Name;
viewModel.Summary = State.Summary;
viewModel.Description = State.Description;
// Tags
EntrySpecificationsViewModel.Tags.Clear();
EntrySpecificationsViewModel.Tags.AddRange(State.Tags);
viewModel.Tags.Clear();
viewModel.Tags.AddRange(State.Tags);
// Categories
EntrySpecificationsViewModel.PreselectedCategories = State.Categories;
viewModel.PreselectedCategories = State.Categories;
// Icon
if (State.Icon != null)
{
State.Icon.Seek(0, SeekOrigin.Begin);
EntrySpecificationsViewModel.IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128);
viewModel.IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128);
}
EntrySpecificationsViewModel = viewModel;
Continue = ReactiveCommand.Create(ExecuteContinue, EntrySpecificationsViewModel.ValidationContext.Valid);
}
private void ApplyToState()
{
if (EntrySpecificationsViewModel == null)
return;
// Basic fields
State.Name = EntrySpecificationsViewModel.Name;
State.Summary = EntrySpecificationsViewModel.Summary;

View File

@ -1,5 +1,3 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;

View File

@ -1,17 +1,16 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.UI.Screens.Workshop.Categories;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Services;
using Avalonia.Media.Imaging;
using IdentityModel;
using ReactiveUI;
using StrawberryShake;
using System;
using System.IO;
using Avalonia.Media.Imaging;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
@ -26,9 +25,9 @@ public class SubmitStepViewModel : SubmissionViewModel
CurrentUser = authenticationService.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Name)?.Value;
GoBack = ReactiveCommand.Create(() => State.ChangeScreen<SpecificationsStepViewModel>());
Continue = ReactiveCommand.Create(() => State.ChangeScreen<UploadStepViewModel>());
ContinueText = "Submit";
this.WhenActivated(d =>
{
if (State.Icon != null)
@ -37,6 +36,7 @@ public class SubmitStepViewModel : SubmissionViewModel
IconBitmap = new Bitmap(State.Icon);
IconBitmap.DisposeWith(d);
}
Observable.FromAsync(workshopClient.GetCategories.ExecuteAsync).Subscribe(PopulateCategories).DisposeWith(d);
});
}
@ -55,19 +55,13 @@ public class SubmitStepViewModel : SubmissionViewModel
set => RaiseAndSetIfChanged(ref _categories, value);
}
public override ReactiveCommand<Unit, Unit> Continue { get; }
public override ReactiveCommand<Unit, Unit> GoBack { get; }
private void PopulateCategories(IOperationResult<IGetCategoriesResult> result)
{
if (result.Data == null)
Categories = null;
else
{
Categories = new ReadOnlyObservableCollection<CategoryViewModel>(
new ObservableCollection<CategoryViewModel>(result.Data.Categories.Where(c => State.Categories.Contains(c.Id)).Select(c => new CategoryViewModel(c)))
);
}
}
}

View File

@ -1,5 +1,3 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;

View File

@ -1,5 +1,4 @@
using System;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
@ -55,12 +54,6 @@ public class UploadStepViewModel : SubmissionViewModel
this.WhenActivated(d => Observable.FromAsync(ExecuteUpload).Subscribe().DisposeWith(d));
}
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> Continue { get; }
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> GoBack { get; } = null!;
public int ProgressPercentage => _progressPercentage.Value;
public bool ProgressIndeterminate => _progressIndeterminate.Value;

View File

@ -27,7 +27,7 @@ public class ValidateEmailStepViewModel : SubmissionViewModel
Continue = ReactiveCommand.Create(ExecuteContinue);
Refresh = ReactiveCommand.CreateFromTask(ExecuteRefresh);
Resend = ReactiveCommand.Create(() => Utilities.OpenUrl(WorkshopConstants.AUTHORITY_URL + "/account/confirm/resend"));
ShowGoBack = false;
ShowHeader = false;
@ -43,12 +43,6 @@ public class ValidateEmailStepViewModel : SubmissionViewModel
});
}
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> Continue { get; }
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> GoBack { get; } = null!;
public ReactiveCommand<Unit, Unit> Refresh { get; }
public ReactiveCommand<Unit, Process?> Resend { get; }

View File

@ -1,6 +1,4 @@
using System;
using System.Linq;
using System.Reactive;
using System.Threading.Tasks;
using Artemis.WebClient.Workshop.Services;
using IdentityModel;
@ -21,12 +19,6 @@ public class WelcomeStepViewModel : SubmissionViewModel
ShowGoBack = false;
}
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> Continue { get; }
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> GoBack { get; } = null!;
private async Task ExecuteContinue()
{
bool loggedIn = await _authenticationService.AutoLogin(true);

View File

@ -6,15 +6,26 @@ namespace Artemis.UI.Screens.Workshop.SubmissionWizard;
public abstract class SubmissionViewModel : ValidatableViewModelBase
{
private ReactiveCommand<Unit, Unit>? _continue;
private ReactiveCommand<Unit, Unit>? _goBack;
private string _continueText = "Continue";
private bool _showFinish;
private bool _showGoBack = true;
private bool _showHeader = true;
public SubmissionWizardState State { get; set; } = null!;
public abstract ReactiveCommand<Unit, Unit> Continue { get; }
public abstract ReactiveCommand<Unit, Unit> GoBack { get; }
public ReactiveCommand<Unit, Unit>? Continue
{
get => _continue;
set => RaiseAndSetIfChanged(ref _continue, value);
}
public ReactiveCommand<Unit, Unit>? GoBack
{
get => _goBack;
set => RaiseAndSetIfChanged(ref _goBack, value);
}
public bool ShowHeader
{

View File

@ -46,7 +46,7 @@
</DataTemplate>
<DataTemplate DataType="system:Object">
<Border Classes="card-condensed" Margin="0,5,5,5 ">
<TextBlock Text="{CompiledBinding Converter={StaticResource JsonConverter}}" FontFamily="{StaticResource RobotoMono}"/>
<TextBlock Text="{CompiledBinding Converter={StaticResource JsonConverter}}" FontFamily="{StaticResource RobotoMono}" FontSize="12"/>
</Border>
</DataTemplate>
</ContentControl.DataTemplates>

View File

@ -16,6 +16,22 @@ public class WorkshopService : IWorkshopService
_router = router;
}
public async Task<Stream?> GetEntryIcon(Guid entryId, CancellationToken cancellationToken)
{
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME);
try
{
HttpResponseMessage response = await client.GetAsync($"entries/{entryId}/icon", cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStreamAsync(cancellationToken);
}
catch (HttpRequestException)
{
// ignored
return null;
}
}
public async Task<ImageUploadResult> SetEntryIcon(Guid entryId, Progress<StreamProgress> progress, Stream icon, CancellationToken cancellationToken)
{
icon.Seek(0, SeekOrigin.Begin);
@ -55,7 +71,7 @@ public class WorkshopService : IWorkshopService
return new IWorkshopService.WorkshopStatus(false, e.Message);
}
}
/// <inheritdoc />
public async Task<bool> ValidateWorkshopStatus(CancellationToken cancellationToken)
{
@ -86,10 +102,11 @@ public class WorkshopService : IWorkshopService
public interface IWorkshopService
{
Task<Stream?> GetEntryIcon(Guid entryId, CancellationToken cancellationToken);
Task<ImageUploadResult> SetEntryIcon(Guid entryId, Progress<StreamProgress> progress, Stream icon, CancellationToken cancellationToken);
Task<WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken);
Task<bool> ValidateWorkshopStatus(CancellationToken cancellationToken);
Task NavigateToEntry(Guid entryId, EntryType entryType);
public record WorkshopStatus(bool IsReachable, string Message);
}