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

Workshop - Added release management

This commit is contained in:
RobertBeekman 2024-04-10 20:59:53 +02:00
parent 7030c7af2a
commit cac44d748d
18 changed files with 578 additions and 174 deletions

View File

@ -49,7 +49,9 @@ namespace Artemis.UI.Routing
new RouteRegistration<WorkshopLibraryViewModel>("library", [
new RouteRegistration<InstalledTabViewModel>("installed"),
new RouteRegistration<SubmissionsTabViewModel>("submissions"),
new RouteRegistration<SubmissionDetailViewModel>("submissions/{entryId:long}")
new RouteRegistration<SubmissionManagementViewModel>("submissions/{entryId:long}", [
new RouteRegistration<SubmissionReleaseViewModel>("releases/{releaseId:long}")
])
])
]),
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),

View File

@ -1,13 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Models;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
@ -24,7 +22,6 @@ public partial class EntryReleasesViewModel : ActivatableViewModelBase
Entry = entry;
Releases = Entry.Releases.OrderByDescending(r => r.CreatedAt).Take(5).Select(r => getEntryReleaseItemViewModel(r)).ToList();
NavigateToRelease = ReactiveCommand.CreateFromTask<IRelease>(ExecuteNavigateToRelease);
this.WhenActivated(d =>
{
@ -35,30 +32,11 @@ public partial class EntryReleasesViewModel : ActivatableViewModelBase
this.WhenAnyValue(vm => vm.SelectedRelease)
.WhereNotNull()
.Subscribe(s => ExecuteNavigateToRelease(s.Release))
.Subscribe(s => _router.Navigate($"/releases/{s.Release.Id}"))
.DisposeWith(d);
});
}
public IEntryDetails Entry { get; }
public List<EntryReleaseItemViewModel> Releases { get; }
public ReactiveCommand<IRelease, Unit> NavigateToRelease { get; }
private async Task ExecuteNavigateToRelease(IRelease release)
{
switch (Entry.EntryType)
{
case EntryType.Profile:
await _router.Navigate($"workshop/entries/profiles/details/{Entry.Id}/releases/{release.Id}");
break;
case EntryType.Layout:
await _router.Navigate($"workshop/entries/layouts/details/{Entry.Id}/releases/{release.Id}");
break;
case EntryType.Plugin:
await _router.Navigate($"workshop/entries/plugins/details/{Entry.Id}/releases/{release.Id}");
break;
default:
throw new ArgumentOutOfRangeException(nameof(Entry.EntryType));
}
}
}

View File

@ -1,85 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:library="clr-namespace:Artemis.UI.Screens.Workshop.Library"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.SubmissionDetailView"
x:DataType="library:SubmissionDetailViewModel">
<UserControl.Resources>
<converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources>
<Grid ColumnDefinitions="300,*,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>
<Border Classes="card-separator" />
<TextBlock Margin="0 0 0 8">
<avalonia:MaterialIcon Kind="Downloads" />
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />
<Run>downloads</Run>
</TextBlock>
<TextBlock Classes="subtitle"
ToolTip.Tip="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Calendar" />
<Run>Created</Run>
<Run Text="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock>
<Border Classes="card-separator" />
<StackPanel Spacing="5">
<Button HorizontalAlignment="Stretch" Command="{CompiledBinding CreateRelease}">
Create new release
</Button>
<Button Classes="danger" HorizontalAlignment="Stretch" Command="{CompiledBinding DeleteSubmission}">
Delete submission
</Button>
</StackPanel>
</StackPanel>
</Border>
<controls:HyperlinkButton Command="{CompiledBinding ViewWorkshopPage}" HorizontalAlignment="Center">
View workshop page
</controls:HyperlinkButton>
</StackPanel>
<ContentControl Grid.Column="1" Grid.Row="0" Content="{CompiledBinding EntrySpecificationsViewModel}"></ContentControl>
<Border Grid.Column="2" Grid.Row="0" Classes="card" Margin="10 0 0 0">
<Grid RowDefinitions="*,Auto">
<ScrollViewer Grid.Row="0" Classes="with-padding" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{CompiledBinding Images}">
<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>
<Button Grid.Row="1" HorizontalAlignment="Stretch" Command="{CompiledBinding AddImage}">Add image</Button>
</Grid>
</Border>
<StackPanel Grid.Column="0" Grid.ColumnSpan="3" 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,11 +0,0 @@
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Library;
public partial class SubmissionDetailView : ReactiveUserControl<SubmissionDetailViewModel>
{
public SubmissionDetailView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,42 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:library="clr-namespace:Artemis.UI.Screens.Workshop.Library"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.SubmissionDetailsView"
x:DataType="library:SubmissionDetailsViewModel">
<Grid ColumnDefinitions="*,300" RowDefinitions="*, Auto">
<ContentControl Grid.Column="0" Grid.Row="0" Content="{CompiledBinding EntrySpecificationsViewModel}"></ContentControl>
<Border Grid.Column="1" Grid.Row="0" Classes="card" Margin="10 0 0 0">
<Grid RowDefinitions="*,Auto">
<ScrollViewer Grid.Row="0" Classes="with-padding" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{CompiledBinding Images}">
<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>
<Button Grid.Row="1" HorizontalAlignment="Stretch" Command="{CompiledBinding AddImage}">Add image</Button>
</Grid>
</Border>
<StackPanel Grid.Column="0" Grid.ColumnSpan="2" 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

@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Library;
public partial class SubmissionDetailsView : ReactiveUserControl<SubmissionDetailsViewModel>
{
public SubmissionDetailsView()
{
InitializeComponent();
}
}

View File

@ -8,8 +8,7 @@ using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.Image;
using Artemis.UI.Screens.Workshop.Parameters;
using Artemis.UI.Screens.Workshop.SubmissionWizard;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
@ -25,7 +24,7 @@ using EntrySpecificationsViewModel = Artemis.UI.Screens.Workshop.Entries.Details
namespace Artemis.UI.Screens.Workshop.Library;
public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailParameters>
public partial class SubmissionDetailsViewModel : RoutableScreen
{
private readonly IWorkshopClient _client;
private readonly IWindowService _windowService;
@ -40,7 +39,7 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
[Notify] private EntrySpecificationsViewModel? _entrySpecificationsViewModel;
[Notify(Setter.Private)] private bool _hasChanges;
public SubmissionDetailViewModel(IWorkshopClient client,
public SubmissionDetailsViewModel(IWorkshopClient client,
IWindowService windowService,
IWorkshopService workshopService,
IRouter router,
@ -56,34 +55,23 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
_getExistingImageSubmissionViewModel = getExistingImageSubmissionViewModel;
_getImageSubmissionViewModel = getImageSubmissionViewModel;
CreateRelease = ReactiveCommand.CreateFromTask(ExecuteCreateRelease);
DeleteSubmission = ReactiveCommand.CreateFromTask(ExecuteDeleteSubmission);
ViewWorkshopPage = ReactiveCommand.CreateFromTask(ExecuteViewWorkshopPage);
AddImage = ReactiveCommand.CreateFromTask(ExecuteAddImage);
DiscardChanges = ReactiveCommand.CreateFromTask(ExecuteDiscardChanges, this.WhenAnyValue(vm => vm.HasChanges));
SaveChanges = ReactiveCommand.CreateFromTask(ExecuteSaveChanges, this.WhenAnyValue(vm => vm.HasChanges));
}
public ObservableCollection<ImageSubmissionViewModel> Images { get; } = new();
public ReactiveCommand<Unit, Unit> CreateRelease { get; }
public ReactiveCommand<Unit, Unit> DeleteSubmission { get; }
public ReactiveCommand<Unit, Unit> ViewWorkshopPage { get; }
public ReactiveCommand<Unit, Unit> AddImage { get; }
public ReactiveCommand<Unit, Unit> SaveChanges { get; }
public ReactiveCommand<Unit, Unit> DiscardChanges { get; }
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
public async Task SetEntry(IGetSubmittedEntryById_Entry? entry, CancellationToken cancellationToken)
{
IOperationResult<IGetSubmittedEntryByIdResult> result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken);
if (result.IsErrorResult())
return;
Entry = result.Data?.Entry;
Entry = entry;
await ApplyDetailsFromEntry(cancellationToken);
ApplyImagesFromEntry();
}
public override async Task OnClosing(NavigationArguments args)
public async Task OnClosing(NavigationArguments args)
{
if (!HasChanges)
return;
@ -243,30 +231,7 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
HasChanges = false;
await _router.Reload();
}
private async Task ExecuteCreateRelease(CancellationToken cancellationToken)
{
if (Entry != null)
await _windowService.ShowDialogAsync<ReleaseWizardViewModel>(Entry);
}
private async Task ExecuteDeleteSubmission(CancellationToken cancellationToken)
{
if (Entry == null)
return;
bool confirmed = await _windowService.ShowConfirmContentDialog(
"Delete submission?",
"You cannot undo this by yourself.\r\n" +
"Users that have already downloaded your submission will keep it.");
if (!confirmed)
return;
IOperationResult<IRemoveEntryResult> result = await _client.RemoveEntry.ExecuteAsync(Entry.Id, cancellationToken);
result.EnsureNoErrors();
await _router.Navigate("workshop/library/submissions");
}
private async Task ExecuteAddImage(CancellationToken arg)
{
string[]? result = await _windowService.CreateOpenFileDialog().WithAllowMultiple().HavingFilter(f => f.WithBitmaps()).ShowAsync();
@ -297,12 +262,6 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
}
}
private async Task ExecuteViewWorkshopPage()
{
if (Entry != null)
await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType);
}
private void InputChanged(object? sender, EventArgs e)
{
UpdateHasChanges();

View File

@ -0,0 +1,80 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:library="clr-namespace:Artemis.UI.Screens.Workshop.Library"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.SubmissionManagementView"
x:DataType="library:SubmissionManagementViewModel">
<UserControl.Resources>
<converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources>
<Grid ColumnDefinitions="300,*" RowDefinitions="*, Auto">
<StackPanel Grid.Column="0" Grid.Row="0" Spacing="10" Margin="0 0 10 0">
<Border Classes="card" VerticalAlignment="Top">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Management</TextBlock>
<Border Classes="card-separator" />
<TextBlock Margin="0 0 0 8">
<avalonia:MaterialIcon Kind="Downloads" />
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />
<Run>downloads</Run>
</TextBlock>
<TextBlock Classes="subtitle" ToolTip.Tip="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Calendar" />
<Run>Created</Run>
<Run Text="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock>
<Border Classes="card-separator" />
<StackPanel Spacing="5">
<Button HorizontalAlignment="Stretch" Command="{CompiledBinding CreateRelease}">
Create new release
</Button>
<Button Classes="danger" HorizontalAlignment="Stretch" Command="{CompiledBinding DeleteSubmission}">
Delete submission
</Button>
</StackPanel>
</StackPanel>
</Border>
<Border Classes="card" IsVisible="{CompiledBinding Releases.Count}">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Releases</TextBlock>
<Border Classes="card-separator" />
<ListBox ItemsSource="{CompiledBinding Releases}" SelectedItem="{CompiledBinding SelectedRelease}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0 5">
<TextBlock Text="{CompiledBinding Version}"></TextBlock>
<TextBlock Classes="subtitle" ToolTip.Tip="{CompiledBinding CreatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Calendar" />
<Run>Created</Run>
<Run Text="{CompiledBinding CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Border>
<controls:HyperlinkButton Command="{CompiledBinding ViewWorkshopPage}" HorizontalAlignment="Center">
View workshop page
</controls:HyperlinkButton>
</StackPanel>
<controls:Frame Grid.Column="1" Grid.Row="0" Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</Grid>
</UserControl>

View File

@ -0,0 +1,17 @@
using System;
using System.Reactive.Disposables;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Library;
public partial class SubmissionManagementView : ReactiveUserControl<SubmissionManagementViewModel>
{
public SubmissionManagementView()
{
InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.Subscribe(screen => RouterFrame.NavigateFromObject(screen ?? ViewModel?.DetailsViewModel))
.DisposeWith(d));
}
}

View File

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.Parameters;
using Artemis.UI.Screens.Workshop.SubmissionWizard;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Library;
public partial class SubmissionManagementViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters>
{
private readonly IWorkshopClient _client;
private readonly IWindowService _windowService;
private readonly IRouter _router;
private readonly IWorkshopService _workshopService;
[Notify] private IGetSubmittedEntryById_Entry? _entry;
[Notify] private List<IGetSubmittedEntryById_Entry_Releases>? _releases;
[Notify] private IGetSubmittedEntryById_Entry_Releases? _selectedRelease;
public SubmissionManagementViewModel(IWorkshopClient client, IRouter router, IWindowService windowService, IWorkshopService workshopService, SubmissionDetailsViewModel detailsViewModel)
{
DetailsViewModel = detailsViewModel;
_client = client;
_router = router;
_windowService = windowService;
_workshopService = workshopService;
this.WhenActivated(d =>
{
this.WhenAnyValue(vm => vm.SelectedRelease)
.WhereNotNull()
.Subscribe(r => _router.Navigate($"/releases/{r.Id}"))
.DisposeWith(d);
});
}
public SubmissionDetailsViewModel DetailsViewModel { get; }
public async Task ViewWorkshopPage()
{
if (Entry != null)
await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType);
}
public async Task CreateRelease()
{
if (Entry != null)
await _windowService.ShowDialogAsync<ReleaseWizardViewModel>(Entry);
}
public async Task DeleteSubmission()
{
if (Entry == null)
return;
bool confirmed = await _windowService.ShowConfirmContentDialog(
"Delete submission?",
"You cannot undo this by yourself.\r\n" +
"Users that have already downloaded your submission will keep it.");
if (!confirmed)
return;
IOperationResult<IRemoveEntryResult> result = await _client.RemoveEntry.ExecuteAsync(Entry.Id);
result.EnsureNoErrors();
await _router.Navigate("workshop/library/submissions");
}
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
// If there is a 2nd parameter, it's a release ID
SelectedRelease = args.RouteParameters.Length > 1 ? Releases?.FirstOrDefault(r => r.Id == (long) args.RouteParameters[1]) : null;
IOperationResult<IGetSubmittedEntryByIdResult> result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken);
if (result.IsErrorResult())
return;
Entry = result.Data?.Entry;
Releases = Entry?.Releases.OrderByDescending(r => r.CreatedAt).ToList();
await DetailsViewModel.SetEntry(Entry, cancellationToken);
}
public override async Task OnClosing(NavigationArguments args)
{
await DetailsViewModel.OnClosing(args);
}
}

View File

@ -0,0 +1,68 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:library="clr-namespace:Artemis.UI.Screens.Workshop.Library"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.SubmissionReleaseView"
x:DataType="library:SubmissionReleaseViewModel">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="5">
<Button VerticalAlignment="Center" Classes="icon-button" Command="{CompiledBinding Close}">
<avalonia:MaterialIcon Kind="ArrowBack" />
</Button>
<TextBlock Classes="h3 no-margin" Text="{CompiledBinding Release.Version}" />
</StackPanel>
<Grid Row="1" ColumnDefinitions="Auto,*">
<Label Grid.Column="0" Target="DescriptionEditor" Margin="0 28 0 0">Changelog</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="2" Grid.Column="0" ColumnDefinitions="*,Auto,*">
<Border Grid.Column="0" BorderThickness="1"
BorderBrush="{DynamicResource TextControlBorderBrush}"
CornerRadius="{DynamicResource ControlCornerRadius}"
Background="{DynamicResource TextControlBackground}"
Padding="{DynamicResource TextControlThemePadding}">
<avaloniaEdit:TextEditor
FontFamily="{StaticResource RobotoMono}"
FontSize="13"
Name="DescriptionEditor"
Document="{CompiledBinding MarkdownDocument}"
WordWrap="True" />
</Border>
<GridSplitter Grid.Column="1" Margin="5 0"></GridSplitter>
<Border Grid.Column="2" Classes="card-condensed">
<mdxaml:MarkdownScrollViewer Margin="5 0"
Name="DescriptionPreview"
Markdown="{CompiledBinding Changelog}"
MarkdownStyleName="FluentAvalonia"
SaveScrollValueWhenContentUpdated="True">
<mdxaml:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml" />
</mdxaml:MarkdownScrollViewer.Styles>
</mdxaml:MarkdownScrollViewer>
</Border>
</Grid>
<StackPanel Grid.Row="3" Margin="0 10 0 0" Orientation="Horizontal" Spacing="5" HorizontalAlignment="Right">
<Button Classes="danger" Command="{CompiledBinding DeleteRelease}">Delete release</Button>
<Button Command="{CompiledBinding Discard}">Discard changes</Button>
<Button Command="{CompiledBinding Save}">Save</Button>
</StackPanel>
</Grid>
</UserControl>

View File

@ -0,0 +1,100 @@
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.ReactiveUI;
using AvaloniaEdit.TextMate;
using ReactiveUI;
using TextMateSharp.Grammars;
using VisualExtensions = Artemis.UI.Shared.Extensions.VisualExtensions;
namespace Artemis.UI.Screens.Workshop.Library;
public partial class SubmissionReleaseView : ReactiveUserControl<SubmissionReleaseViewModel>
{
private ScrollViewer? _editorScrollViewer;
private ScrollViewer? _previewScrollViewer;
private bool _updating;
public SubmissionReleaseView()
{
InitializeComponent();
DescriptionEditor.Options.AllowScrollBelowDocument = false;
RegistryOptions options = new(ThemeName.Dark);
TextMate.Installation? install = DescriptionEditor.InstallTextMate(options);
install.SetGrammar(options.GetScopeByExtension(".md"));
this.WhenActivated(_ => SetupScrollSync());
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
if (this.TryFindResource("SystemAccentColorLight3", out object? resource) && resource is Color color)
DescriptionEditor.TextArea.TextView.LinkTextForegroundBrush = new ImmutableSolidColorBrush(color);
base.OnAttachedToVisualTree(e);
}
private void SetupScrollSync()
{
if (_editorScrollViewer != null)
_editorScrollViewer.PropertyChanged -= EditorScrollViewerOnPropertyChanged;
if (_previewScrollViewer != null)
_previewScrollViewer.PropertyChanged -= PreviewScrollViewerOnPropertyChanged;
_editorScrollViewer = VisualExtensions.GetVisualChildrenOfType<ScrollViewer>(DescriptionEditor).FirstOrDefault();
_previewScrollViewer = VisualExtensions.GetVisualChildrenOfType<ScrollViewer>(DescriptionPreview).FirstOrDefault();
if (_editorScrollViewer != null)
_editorScrollViewer.PropertyChanged += EditorScrollViewerOnPropertyChanged;
if (_previewScrollViewer != null)
_previewScrollViewer.PropertyChanged += PreviewScrollViewerOnPropertyChanged;
}
private void EditorScrollViewerOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property.Name != nameof(ScrollViewer.Offset) || _updating || SynchronizedScrolling.IsChecked != true)
return;
try
{
_updating = true;
SynchronizeScrollViewers(_editorScrollViewer, _previewScrollViewer);
}
finally
{
_updating = false;
}
}
private void PreviewScrollViewerOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property.Name != nameof(ScrollViewer.Offset) || _updating || SynchronizedScrolling.IsChecked != true)
return;
try
{
_updating = true;
SynchronizeScrollViewers(_previewScrollViewer, _editorScrollViewer);
}
finally
{
_updating = false;
}
}
private void SynchronizeScrollViewers(ScrollViewer? source, ScrollViewer? target)
{
if (source == null || target == null)
return;
double sourceScrollableHeight = source.Extent.Height - source.Viewport.Height;
double targetScrollableHeight = target.Extent.Height - target.Viewport.Height;
if (sourceScrollableHeight != 0)
target.Offset = new Vector(target.Offset.X, targetScrollableHeight * (source.Offset.Y / sourceScrollableHeight));
}
}

View File

@ -0,0 +1,124 @@
using System;
using System.Reactive;
using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.Parameters;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Workshop;
using Avalonia.Layout;
using AvaloniaEdit.Document;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Library;
public partial class SubmissionReleaseViewModel : RoutableScreen<ReleaseDetailParameters>
{
private readonly IWorkshopClient _client;
private readonly IRouter _router;
private readonly IWindowService _windowService;
private readonly INotificationService _notificationService;
private readonly ObservableAsPropertyHelper<bool> _hasChanges;
[Notify] private IGetReleaseById_Release? _release;
[Notify] private string _changelog = string.Empty;
[Notify] private TextDocument? _markdownDocument;
public SubmissionReleaseViewModel(IWorkshopClient client, IRouter router, IWindowService windowService, INotificationService notificationService)
{
_client = client;
_router = router;
_windowService = windowService;
_notificationService = notificationService;
_hasChanges = this.WhenAnyValue(vm => vm.Changelog, vm => vm.Release, (current, release) => current != release?.Changelog).ToProperty(this, vm => vm.HasChanges);
Discard = ReactiveCommand.Create(ExecuteDiscard, this.WhenAnyValue(vm => vm.HasChanges));
Save = ReactiveCommand.CreateFromTask(ExecuteSave, this.WhenAnyValue(vm => vm.HasChanges));
this.WhenActivated(d =>
{
Disposable.Create(() =>
{
if (MarkdownDocument != null)
MarkdownDocument.TextChanged -= MarkdownDocumentOnTextChanged;
}).DisposeWith(d);
});
}
public bool HasChanges => _hasChanges.Value;
public ReactiveCommand<Unit, Unit> Discard { get; set; }
public ReactiveCommand<Unit, Unit> Save { get; set; }
public override async Task OnNavigating(ReleaseDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
IOperationResult<IGetReleaseByIdResult> result = await _client.GetReleaseById.ExecuteAsync(parameters.ReleaseId, cancellationToken);
Release = result.Data?.Release;
Changelog = Release?.Changelog ?? string.Empty;
SetupMarkdownDocument();
}
public async Task DeleteRelease()
{
if (Release == null)
return;
bool confirmed = await _windowService.ShowConfirmContentDialog(
"Delete release?",
"This cannot be undone.\r\n" +
"Users that have already downloaded this release will keep it.");
if (!confirmed)
return;
await _client.RemoveRelease.ExecuteAsync(Release.Id);
_notificationService.CreateNotification()
.WithTitle("Deleted release.")
.WithSeverity(NotificationSeverity.Success)
.WithHorizontalPosition(HorizontalAlignment.Left)
.Show();
await Close();
}
public async Task Close()
{
await _router.GoUp();
}
private async Task ExecuteSave(CancellationToken cancellationToken)
{
if (Release == null)
return;
await _client.UpdateRelease.ExecuteAsync(new UpdateReleaseInput {Id = Release.Id, Changelog = Changelog}, cancellationToken);
_notificationService.CreateNotification()
.WithTitle("Saved changelog.")
.WithSeverity(NotificationSeverity.Success)
.WithHorizontalPosition(HorizontalAlignment.Left)
.Show();
}
private void ExecuteDiscard()
{
Changelog = Release?.Changelog ?? string.Empty;
SetupMarkdownDocument();
}
private void SetupMarkdownDocument()
{
if (MarkdownDocument != null)
MarkdownDocument.TextChanged -= MarkdownDocumentOnTextChanged;
MarkdownDocument = new TextDocument(new StringTextSource(Changelog));
MarkdownDocument.TextChanged += MarkdownDocumentOnTextChanged;
}
private void MarkdownDocumentOnTextChanged(object? sender, EventArgs e)
{
Changelog = MarkdownDocument?.Text ?? string.Empty;
}
}

View File

@ -3,3 +3,16 @@ mutation UpdateEntry ($input: UpdateEntryInput!) {
id
}
}
mutation UpdateRelease($input: UpdateReleaseInput!) {
updateRelease(input: $input) {
id
}
}
mutation RemoveRelease($input: Long!) {
removeRelease(id: $input) {
id
}
}

View File

@ -14,5 +14,10 @@ query GetSubmittedEntryById($id: Long!) {
images {
...image
}
releases {
id
version
createdAt
}
}
}

View File

@ -2,10 +2,10 @@ namespace Artemis.WebClient.Workshop;
public static class WorkshopConstants
{
// public const string AUTHORITY_URL = "https://localhost:5001";
// public const string WORKSHOP_URL = "https://localhost:7281";
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
public const string AUTHORITY_URL = "https://localhost:5001";
public const string WORKSHOP_URL = "https://localhost:7281";
// public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
// public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
public const string IDENTITY_CLIENT_NAME = "IdentityApiClient";
public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient";
}

View File

@ -2,7 +2,7 @@ schema: schema.graphql
extensions:
endpoints:
Default GraphQL Endpoint:
url: https://workshop.artemis-rgb.com/graphql
url: https://localhost:7281/graphql
headers:
user-agent: JS GraphQL
introspect: true

View File

@ -100,6 +100,7 @@ type Mutation {
addLayoutInfo(input: CreateLayoutInfoInput!): LayoutInfo
removeEntry(id: Long!): Entry
removeLayoutInfo(id: Long!): LayoutInfo!
removeRelease(id: Long!): Release!
updateEntry(input: UpdateEntryInput!): Entry
updateEntryImage(input: UpdateEntryImageInput!): Image
updateRelease(input: UpdateReleaseInput!): Release