mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-12 13:28:33 +00:00
Merge branch 'development'
This commit is contained in:
commit
058513fd2a
@ -7,6 +7,7 @@ public class LayoutSelection : CorePropertyChanged
|
||||
{
|
||||
private string? _type;
|
||||
private string? _parameter;
|
||||
private string? _errorState;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets what kind of layout reference this is.
|
||||
@ -25,4 +26,13 @@ public class LayoutSelection : CorePropertyChanged
|
||||
get => _parameter;
|
||||
set => SetAndNotify(ref _parameter, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the error state of the layout reference.
|
||||
/// </summary>
|
||||
public string? ErrorState
|
||||
{
|
||||
get => _errorState;
|
||||
set => SetAndNotify(ref _errorState, value);
|
||||
}
|
||||
}
|
||||
@ -184,13 +184,14 @@ internal class DeviceService : IDeviceService
|
||||
device.ApplyLayout(null, false, false);
|
||||
else
|
||||
provider?.ApplyLayout(device, layout);
|
||||
|
||||
UpdateLeds();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
device.LayoutSelection.ErrorState = e.Message;
|
||||
_logger.Error(e, "Failed to apply device layout");
|
||||
}
|
||||
|
||||
UpdateLeds();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -9,5 +9,6 @@ internal interface IRoutableHostScreen : IRoutableScreen
|
||||
{
|
||||
bool RecycleScreen { get; }
|
||||
IRoutableScreen? InternalScreen { get; }
|
||||
IRoutableScreen? InternalDefaultScreen { get; }
|
||||
void InternalChangeScreen(IRoutableScreen? screen);
|
||||
}
|
||||
@ -25,7 +25,13 @@ public abstract class RoutableHostScreen<TScreen> : RoutableScreen, IRoutableHos
|
||||
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the screen to show when no other screen is active.
|
||||
/// </summary>
|
||||
public virtual TScreen? DefaultScreen { get; }
|
||||
|
||||
IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen;
|
||||
IRoutableScreen? IRoutableHostScreen.InternalDefaultScreen => DefaultScreen;
|
||||
|
||||
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
|
||||
{
|
||||
|
||||
@ -27,7 +27,13 @@ public abstract class RoutableHostScreen<TScreen, TParam> : RoutableScreen<TPara
|
||||
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the screen to show when no other screen is active.
|
||||
/// </summary>
|
||||
public virtual TScreen? DefaultScreen { get; }
|
||||
|
||||
IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen;
|
||||
IRoutableScreen? IRoutableHostScreen.InternalDefaultScreen => DefaultScreen;
|
||||
|
||||
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
|
||||
{
|
||||
|
||||
@ -13,6 +13,11 @@ namespace Artemis.UI.Shared.Routing;
|
||||
/// <typeparam name="TParam">The type of parameters the screen expects. It must have a parameterless constructor.</typeparam>
|
||||
public abstract class RoutableScreen<TParam> : RoutableScreen, IRoutableScreen where TParam : new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the parameter source of the screen.
|
||||
/// </summary>
|
||||
protected ParameterSource ParameterSource { get; set; } = ParameterSource.Segment;
|
||||
|
||||
/// <summary>
|
||||
/// Called while navigating to this screen.
|
||||
/// </summary>
|
||||
@ -26,15 +31,16 @@ public abstract class RoutableScreen<TParam> : RoutableScreen, IRoutableScreen w
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||
{
|
||||
Func<object[], TParam> activator = GetParameterActivator();
|
||||
|
||||
if (args.SegmentParameters.Length != _parameterPropertyCount)
|
||||
throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {args.SegmentParameters.Length}.");
|
||||
object[] routeParameters = ParameterSource == ParameterSource.Segment ? args.SegmentParameters : args.RouteParameters;
|
||||
if (routeParameters.Length != _parameterPropertyCount)
|
||||
throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {routeParameters.Length}.");
|
||||
|
||||
TParam parameters = activator(args.SegmentParameters);
|
||||
TParam parameters = activator(routeParameters);
|
||||
await OnNavigating(args, cancellationToken);
|
||||
await OnNavigating(parameters, args, cancellationToken);
|
||||
}
|
||||
@ -97,4 +103,20 @@ public abstract class RoutableScreen<TParam> : RoutableScreen, IRoutableScreen w
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enum representing the source of parameters in the RoutableScreen class.
|
||||
/// </summary>
|
||||
public enum ParameterSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the source where parameters are obtained from the segment of the route.
|
||||
/// </summary>
|
||||
Segment,
|
||||
|
||||
/// <summary>
|
||||
/// Represents the source where parameters are obtained from the entire route.
|
||||
/// </summary>
|
||||
Route
|
||||
}
|
||||
@ -18,6 +18,17 @@ public class RouteRegistration<TViewModel> : IRouterRegistration where TViewMode
|
||||
Route = new Route(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteRegistration{TViewModel}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the route.</param>
|
||||
/// <param name="children">The children of the route.</param>
|
||||
public RouteRegistration(string path, List<IRouterRegistration> children)
|
||||
{
|
||||
Route = new Route(path);
|
||||
Children = children;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
@ -45,6 +45,12 @@ public interface IRouter
|
||||
/// </summary>
|
||||
/// <returns>A task containing a boolean value which indicates whether there was a forward path to go back to.</returns>
|
||||
Task<bool> GoForward();
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously navigates upwards to the parent route.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<bool> GoUp();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the navigation history.
|
||||
|
||||
@ -109,12 +109,11 @@ internal class Navigation
|
||||
// Navigate the child too
|
||||
if (resolution.Child != null)
|
||||
await NavigateResolution(resolution.Child, args, childScreen);
|
||||
// Make sure there is no child
|
||||
else if (childScreen.InternalScreen != null)
|
||||
childScreen.InternalChangeScreen(null);
|
||||
// Without a resolution, navigate to the default screen (which may be null)
|
||||
else if (childScreen.InternalScreen != childScreen.InternalDefaultScreen)
|
||||
childScreen.InternalChangeScreen(childScreen.InternalDefaultScreen);
|
||||
}
|
||||
|
||||
|
||||
Completed = true;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
@ -72,7 +73,13 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
|
||||
/// <inheritdoc />
|
||||
public async Task Navigate(string path, RouterNavigationOptions? options = null)
|
||||
{
|
||||
path = path.ToLower().Trim(' ', '/', '\\');
|
||||
if (path.StartsWith('/') && _currentRouteSubject.Value != null)
|
||||
path = _currentRouteSubject.Value + path;
|
||||
if (path.StartsWith("../") && _currentRouteSubject.Value != null)
|
||||
path = NavigateUp(_currentRouteSubject.Value, path);
|
||||
else
|
||||
path = path.ToLower().Trim(' ', '/', '\\');
|
||||
|
||||
options ??= new RouterNavigationOptions();
|
||||
|
||||
// Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
|
||||
@ -161,6 +168,28 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> GoUp()
|
||||
{
|
||||
string? currentPath = _currentRouteSubject.Value;
|
||||
|
||||
// Keep removing segments until we find a parent route that resolves
|
||||
while (currentPath != null && currentPath.Contains('/'))
|
||||
{
|
||||
string parentPath = currentPath[..currentPath.LastIndexOf('/')];
|
||||
RouteResolution resolution = Resolve(parentPath);
|
||||
if (resolution.Success)
|
||||
{
|
||||
await Navigate(parentPath, new RouterNavigationOptions {AddToHistory = false});
|
||||
return true;
|
||||
}
|
||||
|
||||
currentPath = parentPath;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearHistory()
|
||||
{
|
||||
@ -194,6 +223,24 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
|
||||
|
||||
_logger.Debug("Router disposed, should that be? Stacktrace: \r\n{StackTrace}", Environment.StackTrace);
|
||||
}
|
||||
|
||||
|
||||
private string NavigateUp(string current, string path)
|
||||
{
|
||||
string[] pathParts = current.Split('/');
|
||||
string[] navigateParts = path.Split('/');
|
||||
int upCount = navigateParts.TakeWhile(part => part == "..").Count();
|
||||
|
||||
if (upCount >= pathParts.Length)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot navigate up beyond the root");
|
||||
}
|
||||
|
||||
IEnumerable<string> remainingCurrentPathParts = pathParts.Take(pathParts.Length - upCount);
|
||||
IEnumerable<string> remainingNavigatePathParts = navigateParts.Skip(upCount);
|
||||
|
||||
return string.Join("/", remainingCurrentPathParts.Concat(remainingNavigatePathParts));
|
||||
}
|
||||
|
||||
private void MainWindowServiceOnMainWindowOpened(object? sender, EventArgs e)
|
||||
{
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Margin="20" Grid.Column="0">
|
||||
<StackPanel>
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">TitleTextBlockStyle</TextBlock>
|
||||
<TextBlock Classes="h1">This is heading 1</TextBlock>
|
||||
<TextBlock Classes="h2">This is heading 2</TextBlock>
|
||||
<TextBlock Classes="h3">This is heading 3</TextBlock>
|
||||
@ -22,6 +23,7 @@
|
||||
|
||||
<Grid Margin="20" Grid.Column="1">
|
||||
<StackPanel>
|
||||
<Border Width="400" Classes="skeleton-text title"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h1"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h2"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h3"></Border>
|
||||
@ -39,6 +41,7 @@
|
||||
<Setter Property="Background" Value="#55ff0000"></Setter>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">TitleTextBlockStyle</TextBlock>
|
||||
<TextBlock Classes="h1">This is heading 1</TextBlock>
|
||||
<TextBlock Classes="h2">This is heading 2</TextBlock>
|
||||
<TextBlock Classes="h3">This is heading 3</TextBlock>
|
||||
@ -51,6 +54,7 @@
|
||||
|
||||
<Grid Margin="20" Grid.Column="0" Row="1">
|
||||
<StackPanel Spacing="2">
|
||||
<Border Width="400" Classes="skeleton-text title no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h1 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h2 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h3 no-margin"></Border>
|
||||
@ -68,6 +72,7 @@
|
||||
<Setter Property="Background" Value="#55ff0000"></Setter>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">TitleTextBlockStyle</TextBlock>
|
||||
<TextBlock Classes="h1 no-margin">This is heading 1</TextBlock>
|
||||
<TextBlock Classes="h2 no-margin">This is heading 2</TextBlock>
|
||||
<TextBlock Classes="h3 no-margin">This is heading 3</TextBlock>
|
||||
@ -125,6 +130,11 @@
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.skeleton-text.title">
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="Margin" Value="0 5 0 5" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h1">
|
||||
<Setter Property="Height" Value="65" />
|
||||
<Setter Property="Margin" Value="0 10 0 20" />
|
||||
|
||||
@ -44,5 +44,27 @@
|
||||
<DependentUpon>DeviceSelectionDialogView.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Screens\Workshop\Layout\LayoutListView.axaml.cs">
|
||||
<DependentUpon>LayoutListView.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Screens\Workshop\Plugins\PluginListView.axaml.cs">
|
||||
<DependentUpon>LayoutListView.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Screens\Workshop\Profile\ProfileListView.axaml.cs">
|
||||
<DependentUpon>LayoutListView.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Screens\Workshop\EntryReleases\EntryReleasesView.axaml.cs">
|
||||
<DependentUpon>EntryReleasesView.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<UpToDateCheckInput Remove="Screens\Workshop\Entries\Tabs\PluginListView.axaml" />
|
||||
<UpToDateCheckInput Remove="Screens\Workshop\Entries\Tabs\ProfileListView.axaml" />
|
||||
<UpToDateCheckInput Remove="Screens\Workshop\Plugins\Dialogs\PluginDialogView.axaml" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
51
src/Artemis.UI/Controls/SplitMarkdownEditor.axaml
Normal file
51
src/Artemis.UI/Controls/SplitMarkdownEditor.axaml
Normal file
@ -0,0 +1,51 @@
|
||||
<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:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
|
||||
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
|
||||
xmlns:fa="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Controls.SplitMarkdownEditor">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Row="0" ColumnDefinitions="Auto,*">
|
||||
<Label Grid.Column="0" Name="DescriptionEditorLabel" Target="DescriptionEditor" Margin="0 28 0 0" />
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<CheckBox Name="SynchronizedScrolling" IsChecked="True" VerticalAlignment="Bottom">Synchronized scrolling</CheckBox>
|
||||
<fa:HyperlinkButton
|
||||
Margin="0 0 0 -20"
|
||||
Content="Markdown supported"
|
||||
NavigateUri="https://wiki.artemis-rgb.com/guides/user/markdown?mtm_campaign=artemis&mtm_kwd=markdown-editor"
|
||||
HorizontalAlignment="Right" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" 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"
|
||||
TextChanged="DescriptionEditor_OnTextChanged"
|
||||
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 Document.Text, Mode=OneWay, ElementName=DescriptionEditor}"
|
||||
MarkdownStyleName="FluentAvalonia"
|
||||
SaveScrollValueWhenContentUpdated="True">
|
||||
<mdxaml:MarkdownScrollViewer.Styles>
|
||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
||||
</mdxaml:MarkdownScrollViewer.Styles>
|
||||
</mdxaml:MarkdownScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
150
src/Artemis.UI/Controls/SplitMarkdownEditor.axaml.cs
Normal file
150
src/Artemis.UI/Controls/SplitMarkdownEditor.axaml.cs
Normal file
@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Immutable;
|
||||
using Avalonia.Threading;
|
||||
using AvaloniaEdit.TextMate;
|
||||
using TextMateSharp.Grammars;
|
||||
using VisualExtensions = Artemis.UI.Shared.Extensions.VisualExtensions;
|
||||
|
||||
namespace Artemis.UI.Controls;
|
||||
|
||||
public partial class SplitMarkdownEditor : UserControl
|
||||
{
|
||||
public static readonly StyledProperty<string> TitleProperty = AvaloniaProperty.Register<SplitMarkdownEditor, string>(nameof(Title), string.Empty);
|
||||
public static readonly StyledProperty<string> MarkdownProperty = AvaloniaProperty.Register<SplitMarkdownEditor, string>(nameof(Markdown), string.Empty, defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
private ScrollViewer? _editorScrollViewer;
|
||||
private ScrollViewer? _previewScrollViewer;
|
||||
private bool _scrolling;
|
||||
private bool _updating;
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value);
|
||||
}
|
||||
|
||||
public string Markdown
|
||||
{
|
||||
get => GetValue(MarkdownProperty);
|
||||
set => SetValue(MarkdownProperty, value);
|
||||
}
|
||||
|
||||
public SplitMarkdownEditor()
|
||||
{
|
||||
InitializeComponent();
|
||||
PropertyChanged += OnPropertyChanged;
|
||||
|
||||
DescriptionEditorLabel.Content = Title;
|
||||
DescriptionEditor.Options.AllowScrollBelowDocument = false;
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
if (this.TryFindResource("SystemAccentColorLight3", out object? resource) && resource is Color color)
|
||||
DescriptionEditor.TextArea.TextView.LinkTextForegroundBrush = new ImmutableSolidColorBrush(color);
|
||||
|
||||
SetupScrollSync();
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
// Installing is slow, wait for UI to settle
|
||||
await Task.Delay(300);
|
||||
|
||||
RegistryOptions options = new(ThemeName.Dark);
|
||||
TextMate.Installation? install = DescriptionEditor.InstallTextMate(options);
|
||||
install.SetGrammar(options.GetScopeByExtension(".md"));
|
||||
}, DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
|
||||
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) || _scrolling || SynchronizedScrolling.IsChecked != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_scrolling = true;
|
||||
SynchronizeScrollViewers(_editorScrollViewer, _previewScrollViewer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_scrolling = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewScrollViewerOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property.Name != nameof(ScrollViewer.Offset) || _scrolling || SynchronizedScrolling.IsChecked != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_scrolling = true;
|
||||
SynchronizeScrollViewers(_previewScrollViewer, _editorScrollViewer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_scrolling = 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));
|
||||
}
|
||||
|
||||
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property == TitleProperty)
|
||||
DescriptionEditorLabel.Content = Title;
|
||||
else if (e.Property == MarkdownProperty && DescriptionEditor.Text != Markdown)
|
||||
{
|
||||
try
|
||||
{
|
||||
_updating = true;
|
||||
DescriptionEditor.Clear();
|
||||
DescriptionEditor.AppendText(Markdown);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_updating = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void DescriptionEditor_OnTextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (!_updating && Markdown != DescriptionEditor.Text)
|
||||
Markdown = DescriptionEditor.Text;
|
||||
}
|
||||
}
|
||||
@ -7,77 +7,65 @@ using Artemis.UI.Screens.Settings.Updating;
|
||||
using Artemis.UI.Screens.SurfaceEditor;
|
||||
using Artemis.UI.Screens.Workshop;
|
||||
using Artemis.UI.Screens.Workshop.Entries;
|
||||
using Artemis.UI.Screens.Workshop.Entries.Tabs;
|
||||
using Artemis.UI.Screens.Workshop.EntryReleases;
|
||||
using Artemis.UI.Screens.Workshop.Home;
|
||||
using Artemis.UI.Screens.Workshop.Layout;
|
||||
using Artemis.UI.Screens.Workshop.Library;
|
||||
using Artemis.UI.Screens.Workshop.Library.Tabs;
|
||||
using Artemis.UI.Screens.Workshop.Plugins;
|
||||
using Artemis.UI.Screens.Workshop.Profile;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using PluginDetailsViewModel = Artemis.UI.Screens.Workshop.Plugins.PluginDetailsViewModel;
|
||||
|
||||
namespace Artemis.UI.Routing;
|
||||
|
||||
public static class Routes
|
||||
namespace Artemis.UI.Routing
|
||||
{
|
||||
public static List<IRouterRegistration> ArtemisRoutes =
|
||||
[
|
||||
new RouteRegistration<BlankViewModel>("blank"),
|
||||
new RouteRegistration<HomeViewModel>("home"),
|
||||
new RouteRegistration<WorkshopViewModel>("workshop")
|
||||
{
|
||||
Children =
|
||||
[
|
||||
public static class Routes
|
||||
{
|
||||
public static readonly List<IRouterRegistration> ArtemisRoutes =
|
||||
[
|
||||
new RouteRegistration<BlankViewModel>("blank"),
|
||||
new RouteRegistration<HomeViewModel>("home"),
|
||||
new RouteRegistration<WorkshopViewModel>("workshop", [
|
||||
new RouteRegistration<WorkshopOfflineViewModel>("offline/{message:string}"),
|
||||
new RouteRegistration<EntriesViewModel>("entries")
|
||||
{
|
||||
Children =
|
||||
[
|
||||
new RouteRegistration<PluginListViewModel>("plugins")
|
||||
{
|
||||
Children = [new RouteRegistration<PluginDetailsViewModel>("details/{entryId:long}")]
|
||||
},
|
||||
new RouteRegistration<ProfileListViewModel>("profiles")
|
||||
{
|
||||
Children = [new RouteRegistration<ProfileDetailsViewModel>("details/{entryId:long}")]
|
||||
},
|
||||
new RouteRegistration<LayoutListViewModel>("layouts")
|
||||
{
|
||||
Children = [new RouteRegistration<LayoutDetailsViewModel>("details/{entryId:long}")]
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
new RouteRegistration<WorkshopLibraryViewModel>("library")
|
||||
{
|
||||
Children =
|
||||
[
|
||||
new RouteRegistration<InstalledTabViewModel>("installed"),
|
||||
new RouteRegistration<SubmissionsTabViewModel>("submissions"),
|
||||
new RouteRegistration<SubmissionDetailViewModel>("submissions/{entryId:long}")
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
|
||||
new RouteRegistration<SettingsViewModel>("settings")
|
||||
{
|
||||
Children =
|
||||
[
|
||||
new RouteRegistration<EntriesViewModel>("entries", [
|
||||
new RouteRegistration<PluginListViewModel>("plugins", [
|
||||
new RouteRegistration<PluginDetailsViewModel>("details/{entryId:long}", [
|
||||
new RouteRegistration<PluginManageViewModel>("manage"),
|
||||
new RouteRegistration<EntryReleaseViewModel>("releases/{releaseId:long}")
|
||||
])
|
||||
]),
|
||||
new RouteRegistration<ProfileListViewModel>("profiles", [
|
||||
new RouteRegistration<ProfileDetailsViewModel>("details/{entryId:long}", [
|
||||
new RouteRegistration<EntryReleaseViewModel>("releases/{releaseId:long}")
|
||||
])
|
||||
]),
|
||||
new RouteRegistration<LayoutListViewModel>("layouts", [
|
||||
new RouteRegistration<LayoutDetailsViewModel>("details/{entryId:long}", [
|
||||
new RouteRegistration<LayoutManageViewModel>("manage"),
|
||||
new RouteRegistration<EntryReleaseViewModel>("releases/{releaseId:long}")
|
||||
])
|
||||
])
|
||||
]),
|
||||
new RouteRegistration<WorkshopLibraryViewModel>("library", [
|
||||
new RouteRegistration<InstalledTabViewModel>("installed"),
|
||||
new RouteRegistration<SubmissionsTabViewModel>("submissions"),
|
||||
new RouteRegistration<SubmissionManagementViewModel>("submissions/{entryId:long}", [
|
||||
new RouteRegistration<SubmissionReleaseViewModel>("releases/{releaseId:long}")
|
||||
])
|
||||
])
|
||||
]),
|
||||
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
|
||||
new RouteRegistration<SettingsViewModel>("settings", [
|
||||
new RouteRegistration<GeneralTabViewModel>("general"),
|
||||
new RouteRegistration<PluginsTabViewModel>("plugins"),
|
||||
new RouteRegistration<DevicesTabViewModel>("devices"),
|
||||
new RouteRegistration<ReleasesTabViewModel>("releases")
|
||||
{
|
||||
Children = [new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")]
|
||||
},
|
||||
|
||||
new RouteRegistration<ReleasesTabViewModel>("releases", [
|
||||
new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")
|
||||
]),
|
||||
new RouteRegistration<AccountTabViewModel>("account"),
|
||||
new RouteRegistration<AboutTabViewModel>("about")
|
||||
]
|
||||
},
|
||||
|
||||
new RouteRegistration<ProfileEditorViewModel>("profile-editor/{profileConfigurationId:guid}")
|
||||
];
|
||||
]),
|
||||
new RouteRegistration<ProfileEditorViewModel>("profile-editor/{profileConfigurationId:guid}")
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -9,100 +9,111 @@
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
||||
x:Class="Artemis.UI.Screens.Device.Layout.DeviceLayoutTabView"
|
||||
x:DataType="layout:DeviceLayoutTabViewModel">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="card" Margin="5">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<StackPanel Grid.Row="0">
|
||||
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Row="0" Grid.Column="0">
|
||||
<TextBlock Text="Default layout file path" />
|
||||
<TextBlock Classes="subtitle" FontSize="12" TextWrapping="Wrap" Text="{CompiledBinding DefaultLayoutPath}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Button
|
||||
Classes="icon-button"
|
||||
HorizontalAlignment="Right"
|
||||
IsEnabled="{CompiledBinding !!DefaultLayoutPath}"
|
||||
ToolTip.Tip="Copy layout file path to clipboard"
|
||||
Click="LayoutPathButton_OnClick">
|
||||
<avalonia:MaterialIcon Kind="ContentCopy" Width="18" Height="18" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<controls:InfoBar Grid.Row="0"
|
||||
Title="Failed to apply layout"
|
||||
IsOpen="{CompiledBinding Device.LayoutSelection.ErrorState, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
||||
Message="{CompiledBinding Device.LayoutSelection.ErrorState}"
|
||||
Severity="Error"
|
||||
IsClosable="False"
|
||||
Margin="5 0"/>
|
||||
|
||||
<Border Classes="card-separator" />
|
||||
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Row="1" Grid.Column="0">
|
||||
<TextBlock Text="Image file path" />
|
||||
<TextBlock Classes="subtitle" FontSize="12" TextWrapping="Wrap" Text="{CompiledBinding ImagePath, TargetNullValue=None}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Button
|
||||
Classes="icon-button"
|
||||
HorizontalAlignment="Right"
|
||||
IsEnabled="{CompiledBinding !!ImagePath}"
|
||||
ToolTip.Tip="Copy image file path to clipboard"
|
||||
Click="ImagePathButton_OnClick">
|
||||
<avalonia:MaterialIcon Kind="ContentCopy" Width="18" Height="18" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="card" Margin="5">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<StackPanel Grid.Row="0">
|
||||
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Row="0" Grid.Column="0">
|
||||
<TextBlock Text="Default layout file path" />
|
||||
<TextBlock Classes="subtitle" FontSize="12" TextWrapping="Wrap" Text="{CompiledBinding DefaultLayoutPath}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Button
|
||||
Classes="icon-button"
|
||||
HorizontalAlignment="Right"
|
||||
IsEnabled="{CompiledBinding !!DefaultLayoutPath}"
|
||||
ToolTip.Tip="Copy layout file path to clipboard"
|
||||
Click="LayoutPathButton_OnClick">
|
||||
<avalonia:MaterialIcon Kind="ContentCopy" Width="18" Height="18" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Border Classes="card-separator" />
|
||||
<Grid RowDefinitions="*,*,*" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Row="1" Grid.Column="0">
|
||||
<TextBlock Text="Layout provider" />
|
||||
<TextBlock Classes="subtitle" FontSize="12" Text="Choose between different ways to load a layout for this device." />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="ComboBox.layoutProvider /template/ ContentControl#ContentPresenter">
|
||||
<Setter Property="ContentTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
|
||||
<Border Classes="card-separator" />
|
||||
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Row="1" Grid.Column="0">
|
||||
<TextBlock Text="Image file path" />
|
||||
<TextBlock Classes="subtitle" FontSize="12" TextWrapping="Wrap" Text="{CompiledBinding ImagePath, TargetNullValue=None}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Button
|
||||
Classes="icon-button"
|
||||
HorizontalAlignment="Right"
|
||||
IsEnabled="{CompiledBinding !!ImagePath}"
|
||||
ToolTip.Tip="Copy image file path to clipboard"
|
||||
Click="ImagePathButton_OnClick">
|
||||
<avalonia:MaterialIcon Kind="ContentCopy" Width="18" Height="18" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Border Classes="card-separator" />
|
||||
<Grid RowDefinitions="*,*,*" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Row="1" Grid.Column="0">
|
||||
<TextBlock Text="Layout provider" />
|
||||
<TextBlock Classes="subtitle" FontSize="12" Text="Choose between different ways to load a layout for this device." />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="ComboBox.layoutProvider /template/ ContentControl#ContentPresenter">
|
||||
<Setter Property="ContentTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
|
||||
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
|
||||
</DataTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<ComboBox Classes="layoutProvider"
|
||||
Width="150"
|
||||
SelectedItem="{CompiledBinding SelectedLayoutProvider}"
|
||||
ItemsSource="{CompiledBinding LayoutProviders}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
|
||||
</DataTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<ComboBox Classes="layoutProvider"
|
||||
Width="150"
|
||||
SelectedItem="{CompiledBinding SelectedLayoutProvider}"
|
||||
ItemsSource="{CompiledBinding LayoutProviders}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
|
||||
<TextBlock Classes="subtitle" Text="{CompiledBinding Description}" TextWrapping="Wrap" MaxWidth="350" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<TextBlock Classes="subtitle" Text="{CompiledBinding Description}" TextWrapping="Wrap" MaxWidth="350" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<ContentControl Content="{CompiledBinding SelectedLayoutProvider}" ClipToBounds="False" />
|
||||
<ContentControl Content="{CompiledBinding SelectedLayoutProvider}" ClipToBounds="False" />
|
||||
|
||||
<Border Classes="card-separator" />
|
||||
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Row="1" Grid.Column="0">
|
||||
<TextBlock Text="Export current layout" />
|
||||
<TextBlock Classes="subtitle" FontSize="12" Text="If there is a layout used, export that. Otherwise, export the LEDs present." />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal">
|
||||
<Button HorizontalAlignment="Right" Content="Export" Command="{CompiledBinding ExportLayout}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<controls:HyperlinkButton
|
||||
Grid.Row="1"
|
||||
Content="Learn more about layouts on the wiki"
|
||||
NavigateUri="https://wiki.artemis-rgb.com/en/guides/developer/layouts?mtm_campaign=artemis&mtm_kwd=device-properties"
|
||||
Margin="0 20"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<Border Classes="card-separator" />
|
||||
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Row="1" Grid.Column="0">
|
||||
<TextBlock Text="Export current layout" />
|
||||
<TextBlock Classes="subtitle" FontSize="12" Text="If there is a layout used, export that. Otherwise, export the LEDs present." />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal">
|
||||
<Button HorizontalAlignment="Right" Content="Export" Command="{CompiledBinding ExportLayout}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<controls:HyperlinkButton
|
||||
Grid.Row="1"
|
||||
Content="Learn more about layouts on the wiki"
|
||||
NavigateUri="https://wiki.artemis-rgb.com/en/guides/developer/layouts?mtm_campaign=artemis&mtm_kwd=device-properties"
|
||||
Margin="0 20"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@ -34,7 +34,7 @@ public abstract partial class TreeItemViewModel : ActivatableViewModelBase
|
||||
private RenderProfileElement? _currentProfileElement;
|
||||
private ObservableAsPropertyHelper<bool>? _isFocused;
|
||||
private TimeSpan _time;
|
||||
|
||||
|
||||
[Notify] private bool _canPaste;
|
||||
[Notify] private bool _isExpanded;
|
||||
[Notify] private bool _isFlyoutOpen;
|
||||
@ -100,7 +100,7 @@ public abstract partial class TreeItemViewModel : ActivatableViewModelBase
|
||||
public ReactiveCommand<Unit, Unit> Paste { get; }
|
||||
public ReactiveCommand<Unit, Unit> Delete { get; }
|
||||
public abstract bool SupportsChildren { get; }
|
||||
|
||||
|
||||
public async Task ShowBrokenStateExceptions()
|
||||
{
|
||||
if (ProfileElement == null)
|
||||
@ -117,7 +117,7 @@ public abstract partial class TreeItemViewModel : ActivatableViewModelBase
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void InsertElement(TreeItemViewModel elementViewModel, int targetIndex)
|
||||
{
|
||||
if (elementViewModel.Parent == this && Children.IndexOf(elementViewModel) == targetIndex)
|
||||
@ -239,26 +239,22 @@ public abstract partial class TreeItemViewModel : ActivatableViewModelBase
|
||||
await _windowService.ShowDialogAsync<LayerHintsDialogViewModel, bool>(layer);
|
||||
await ProfileEditorService.SaveProfileAsync();
|
||||
}
|
||||
|
||||
|
||||
private void ExecuteApplyAdaptionHints()
|
||||
{
|
||||
if (ProfileElement is not Layer layer)
|
||||
return;
|
||||
|
||||
|
||||
ProfileEditorService.ExecuteCommand(new ApplyAdaptionHints(layer, _deviceService.EnabledDevices.ToList()));
|
||||
}
|
||||
|
||||
private async void UpdateCanPaste(bool isFlyoutOpen)
|
||||
{
|
||||
string[] formats = await Shared.UI.Clipboard.GetFormatsAsync();
|
||||
//diogotr7: This can be null on Linux sometimes. I'm not sure why.
|
||||
if (formats == null!)
|
||||
{
|
||||
CanPaste = false;
|
||||
return;
|
||||
}
|
||||
|
||||
CanPaste = formats.Contains(ProfileElementExtensions.ClipboardDataFormat);
|
||||
string[]? formats = await Shared.UI.Clipboard.GetFormatsAsync();
|
||||
|
||||
// Can be null on some platforms
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||
CanPaste = formats != null && formats.Contains(ProfileElementExtensions.ClipboardDataFormat);
|
||||
}
|
||||
|
||||
private bool GetIsFocused(ProfileEditorFocusMode focusMode, RenderProfileElement? currentProfileElement)
|
||||
|
||||
@ -174,8 +174,11 @@ public partial class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, IT
|
||||
|
||||
private async void UpdateCanPaste(bool isFlyoutOpen)
|
||||
{
|
||||
string[] formats = await Shared.UI.Clipboard.GetFormatsAsync();
|
||||
CanPaste = formats.Contains("Artemis.Keyframes");
|
||||
string[]? formats = await Shared.UI.Clipboard.GetFormatsAsync();
|
||||
|
||||
// Can be null on some platforms
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||
CanPaste = formats != null && formats.Contains("Artemis.Keyframes");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -19,7 +19,7 @@ public partial class RootView : ReactiveUserControl<RootViewModel>
|
||||
{
|
||||
try
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => RootFrame.NavigateFromObject(viewModel));
|
||||
RootFrame.NavigateFromObject(viewModel);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@ -18,7 +18,7 @@ public partial class SettingsView : ReactiveUserControl<SettingsViewModel>
|
||||
|
||||
private void Navigate(ViewModelBase viewModel)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
|
||||
TabFrame.NavigateFromObject(viewModel);
|
||||
}
|
||||
|
||||
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
|
||||
|
||||
@ -17,17 +17,13 @@ public partial class ReleasesTabView : ReactiveUserControl<ReleasesTabViewModel>
|
||||
|
||||
private void Navigate(ViewModelBase viewModel)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
ReleaseFrame.NavigateFromObject(viewModel);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
});
|
||||
ReleaseFrame.NavigateFromObject(viewModel);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -43,7 +43,7 @@
|
||||
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Classes="h4 no-margin">Release info</TextBlock>
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Release info</TextBlock>
|
||||
|
||||
<Panel Grid.Column="1" IsVisible="{CompiledBinding InstallationAvailable}">
|
||||
<!-- Install progress -->
|
||||
@ -124,7 +124,7 @@
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Column="2" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Right">
|
||||
<avalonia:MaterialIcon Kind="BoxOutline" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||
<avalonia:MaterialIcon Kind="File" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">File size</TextBlock>
|
||||
<TextBlock Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
@ -149,5 +149,4 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@ -13,71 +13,90 @@
|
||||
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
|
||||
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||
</UserControl.Resources>
|
||||
<StackPanel>
|
||||
<Panel>
|
||||
<Border CornerRadius="6"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0 0 10 0"
|
||||
Width="80"
|
||||
Height="80"
|
||||
ClipToBounds="True">
|
||||
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
|
||||
</Border>
|
||||
<Button Classes="icon-button"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{CompiledBinding CopyShareLink}"
|
||||
ToolTip.Tip="Copy share link">
|
||||
<avalonia:MaterialIcon Kind="ShareVariant" />
|
||||
<Panel>
|
||||
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}">
|
||||
<Border Classes="skeleton-text" Margin="0 0 10 0" Width="80" Height="80"></Border>
|
||||
<Border Classes="skeleton-text title" HorizontalAlignment="Stretch"/>
|
||||
<Border Classes="skeleton-text" Width="120"/>
|
||||
<Border Classes="skeleton-text" Width="140" Margin="0 8"/>
|
||||
<Border Classes="skeleton-text" Width="80"/>
|
||||
<Border Classes="card-separator" Margin="0 15 0 17"></Border>
|
||||
<Border Classes="skeleton-text" Width="120"/>
|
||||
<StackPanel Margin="0 10 0 0">
|
||||
<Border Classes="skeleton-text" Width="160"/>
|
||||
<Border Classes="skeleton-text" Width="160"/>
|
||||
</StackPanel>
|
||||
<Border Classes="skeleton-button"></Border>
|
||||
</StackPanel>
|
||||
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<Panel>
|
||||
<Border CornerRadius="6"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0 0 10 0"
|
||||
Width="80"
|
||||
Height="80"
|
||||
ClipToBounds="True">
|
||||
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
|
||||
</Border>
|
||||
<Button Classes="icon-button"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{CompiledBinding CopyShareLink}"
|
||||
ToolTip.Tip="Copy share link">
|
||||
<avalonia:MaterialIcon Kind="ShareVariant" />
|
||||
</Button>
|
||||
</Panel>
|
||||
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}"
|
||||
MaxLines="3"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Text="{CompiledBinding Entry.Name, FallbackValue=Title}"/>
|
||||
|
||||
<TextBlock Classes="subtitle" TextTrimming="CharacterEllipsis" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
|
||||
|
||||
<TextBlock Margin="0 8" TextWrapping="Wrap" Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}" />
|
||||
|
||||
<!-- Categories -->
|
||||
<ItemsControl ItemsSource="{CompiledBinding Entry.Categories}" Margin="0 0 -8 0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal"></WrapPanel>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Margin="0 0 8 0">
|
||||
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia:MaterialIcon>
|
||||
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Border Classes="card-separator"></Border>
|
||||
|
||||
<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>
|
||||
<TextBlock Classes="subtitle"
|
||||
ToolTip.Tip="{CompiledBinding UpdatedAt, Converter={StaticResource DateTimeConverter}}">
|
||||
<avalonia:MaterialIcon Kind="Update" />
|
||||
<Run>Updated</Run>
|
||||
<Run Text="{CompiledBinding UpdatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
|
||||
</TextBlock>
|
||||
|
||||
<Button IsVisible="{CompiledBinding CanBeManaged}" Command="{CompiledBinding GoToManage}" Margin="0 10 0 0" HorizontalAlignment="Stretch">
|
||||
Manage installation
|
||||
</Button>
|
||||
</Panel>
|
||||
|
||||
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}"
|
||||
MaxLines="3"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Text="{CompiledBinding Entry.Name, FallbackValue=Title }" />
|
||||
|
||||
<TextBlock Classes="subtitle" TextTrimming="CharacterEllipsis" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
|
||||
|
||||
<TextBlock Margin="0 8" TextWrapping="Wrap" Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}" />
|
||||
|
||||
<!-- Categories -->
|
||||
<ItemsControl ItemsSource="{CompiledBinding Entry.Categories}" Margin="0 0 -8 0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal"></WrapPanel>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Margin="0 0 8 0">
|
||||
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia:MaterialIcon>
|
||||
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Border Classes="card-separator"></Border>
|
||||
|
||||
<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>
|
||||
<TextBlock Classes="subtitle"
|
||||
ToolTip.Tip="{CompiledBinding UpdatedAt, Converter={StaticResource DateTimeConverter}}">
|
||||
<avalonia:MaterialIcon Kind="Update" />
|
||||
<Run>Updated</Run>
|
||||
<Run Text="{CompiledBinding UpdatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</UserControl>
|
||||
@ -1,10 +1,11 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.Details;
|
||||
|
||||
public partial class EntryInfoView : UserControl
|
||||
public partial class EntryInfoView : ReactiveUserControl<EntryInfoViewModel>
|
||||
{
|
||||
public EntryInfoView()
|
||||
{
|
||||
|
||||
@ -1,28 +1,66 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.WebClient.Workshop.Extensions;
|
||||
using Artemis.WebClient.Workshop.Models;
|
||||
using Artemis.WebClient.Workshop.Services;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.Details;
|
||||
|
||||
public class EntryInfoViewModel : ViewModelBase
|
||||
public partial class EntryInfoViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly IRouter _router;
|
||||
private readonly INotificationService _notificationService;
|
||||
public IEntryDetails Entry { get; }
|
||||
public DateTimeOffset? UpdatedAt { get; }
|
||||
|
||||
public EntryInfoViewModel(IEntryDetails entry, INotificationService notificationService)
|
||||
private readonly IWorkshopService _workshopService;
|
||||
[Notify] private IEntryDetails? _entry;
|
||||
[Notify] private DateTimeOffset? _updatedAt;
|
||||
[Notify] private bool _canBeManaged;
|
||||
|
||||
public EntryInfoViewModel(IRouter router, INotificationService notificationService, IWorkshopService workshopService)
|
||||
{
|
||||
_router = router;
|
||||
_notificationService = notificationService;
|
||||
_workshopService = workshopService;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
Observable.FromEventPattern<InstalledEntry>(x => workshopService.OnInstalledEntrySaved += x, x => workshopService.OnInstalledEntrySaved -= x)
|
||||
.StartWith([])
|
||||
.Subscribe(_ => CanBeManaged = Entry != null && Entry.EntryType != EntryType.Profile && workshopService.GetInstalledEntry(Entry.Id) != null)
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public void SetEntry(IEntryDetails? entry)
|
||||
{
|
||||
Entry = entry;
|
||||
UpdatedAt = Entry.LatestRelease?.CreatedAt ?? Entry.CreatedAt;
|
||||
UpdatedAt = Entry != null && Entry.Releases.Any() ? Entry.Releases.Max(r => r.CreatedAt) : Entry?.CreatedAt;
|
||||
CanBeManaged = Entry != null && Entry.EntryType != EntryType.Profile && _workshopService.GetInstalledEntry(Entry.Id) != null;
|
||||
}
|
||||
|
||||
public async Task CopyShareLink()
|
||||
{
|
||||
if (Entry == null)
|
||||
return;
|
||||
|
||||
await Shared.UI.Clipboard.SetTextAsync($"{WorkshopConstants.WORKSHOP_URL}/entries/{Entry.Id}/{StringUtilities.UrlFriendly(Entry.Name)}");
|
||||
_notificationService.CreateNotification().WithTitle("Copied share link to clipboard.").Show();
|
||||
}
|
||||
|
||||
public async Task GoToManage()
|
||||
{
|
||||
if (Entry == null)
|
||||
return;
|
||||
|
||||
await _router.Navigate($"{Entry.GetEntryPath()}/manage");
|
||||
}
|
||||
}
|
||||
@ -1,52 +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:details="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Details"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntryReleasesView"
|
||||
x:DataType="details:EntryReleasesViewModel">
|
||||
<UserControl.Resources>
|
||||
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||
<sharedConverters:BytesToStringConverter x:Key="BytesToStringConverter" />
|
||||
</UserControl.Resources>
|
||||
<StackPanel>
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Latest release</TextBlock>
|
||||
<Border Classes="card-separator" />
|
||||
<Button HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Command="{CompiledBinding DownloadLatestRelease}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<!-- Icon -->
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="4"
|
||||
Background="{StaticResource SystemAccentColor}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 6"
|
||||
Width="50"
|
||||
Height="50"
|
||||
ClipToBounds="True">
|
||||
<avalonia:MaterialIcon Kind="Download"></avalonia:MaterialIcon>
|
||||
</Border>
|
||||
|
||||
<!-- Body -->
|
||||
<StackPanel Grid.Column="1" Margin="10 0" VerticalAlignment="Center">
|
||||
<TextBlock Text="{CompiledBinding Entry.LatestRelease.Version, FallbackValue=Version}"></TextBlock>
|
||||
<TextBlock Classes="subtitle">
|
||||
<avalonia:MaterialIcon Kind="BoxOutline" />
|
||||
<Run Text="{CompiledBinding Entry.LatestRelease.DownloadSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay}"></Run>
|
||||
</TextBlock>
|
||||
<TextBlock Classes="subtitle"
|
||||
ToolTip.Tip="{CompiledBinding Entry.LatestRelease.CreatedAt, Converter={StaticResource DateTimeConverter}}">
|
||||
<avalonia:MaterialIcon Kind="Calendar" />
|
||||
<Run>Created</Run>
|
||||
<Run Text="{CompiledBinding Entry.LatestRelease.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -1,13 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.Details;
|
||||
|
||||
public partial class EntryReleasesView : UserControl
|
||||
{
|
||||
public EntryReleasesView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
using System;
|
||||
using System.Reactive;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.Builders;
|
||||
using Artemis.UI.Shared.Utilities;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
||||
using Artemis.WebClient.Workshop.Models;
|
||||
using Humanizer;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.Details;
|
||||
|
||||
public class EntryReleasesViewModel : ViewModelBase
|
||||
{
|
||||
private readonly EntryInstallationHandlerFactory _factory;
|
||||
private readonly IWindowService _windowService;
|
||||
private readonly INotificationService _notificationService;
|
||||
|
||||
public EntryReleasesViewModel(IEntryDetails entry, EntryInstallationHandlerFactory factory, IWindowService windowService, INotificationService notificationService)
|
||||
{
|
||||
_factory = factory;
|
||||
_windowService = windowService;
|
||||
_notificationService = notificationService;
|
||||
|
||||
Entry = entry;
|
||||
DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease);
|
||||
OnInstallationStarted = Confirm;
|
||||
}
|
||||
|
||||
public IEntryDetails Entry { get; }
|
||||
public ReactiveCommand<Unit, Unit> DownloadLatestRelease { get; }
|
||||
|
||||
public Func<IEntryDetails, Task<bool>> OnInstallationStarted { get; set; }
|
||||
public Func<InstalledEntry, Task>? OnInstallationFinished { get; set; }
|
||||
|
||||
private async Task ExecuteDownloadLatestRelease(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Entry.LatestRelease == null)
|
||||
return;
|
||||
|
||||
if (await OnInstallationStarted(Entry))
|
||||
return;
|
||||
|
||||
IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType);
|
||||
EntryInstallResult result = await installationHandler.InstallAsync(Entry, Entry.LatestRelease, new Progress<StreamProgress>(), cancellationToken);
|
||||
if (result.IsSuccess && result.Entry != null)
|
||||
{
|
||||
if (OnInstallationFinished != null)
|
||||
await OnInstallationFinished(result.Entry);
|
||||
_notificationService.CreateNotification().WithTitle($"{Entry.EntryType.Humanize(LetterCasing.Sentence)} installed").WithSeverity(NotificationSeverity.Success).Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
_notificationService.CreateNotification()
|
||||
.WithTitle($"Failed to install {Entry.EntryType.Humanize(LetterCasing.LowerCase)}")
|
||||
.WithMessage(result.Message)
|
||||
.WithSeverity(NotificationSeverity.Error).Show();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> Confirm(IEntryDetails entryDetails)
|
||||
{
|
||||
bool confirm = await _windowService.ShowConfirmContentDialog(
|
||||
"Install latest release",
|
||||
$"Are you sure you want to download and install version {entryDetails.LatestRelease?.Version} of {entryDetails.Name}?"
|
||||
);
|
||||
|
||||
return !confirm;
|
||||
}
|
||||
}
|
||||
@ -2,18 +2,15 @@
|
||||
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:entries="clr-namespace:Artemis.UI.Screens.Workshop.Entries"
|
||||
xmlns:tagsInput="clr-namespace:Artemis.UI.Shared.TagsInput;assembly=Artemis.UI.Shared"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:categories="clr-namespace:Artemis.UI.Screens.Workshop.Categories"
|
||||
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"
|
||||
xmlns:details="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Details"
|
||||
xmlns:controls="clr-namespace:Artemis.UI.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntrySpecificationsView"
|
||||
x:DataType="details:EntrySpecificationsViewModel">
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<StackPanel>
|
||||
<StackPanel.Styles>
|
||||
<Styles>
|
||||
@ -95,48 +92,9 @@
|
||||
<tagsInput:TagsInput Tags="{CompiledBinding Tags}" />
|
||||
</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&mtm_kwd=markdown-editor"
|
||||
HorizontalAlignment="Right"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="2" 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 Description}"
|
||||
MarkdownStyleName="FluentAvalonia"
|
||||
SaveScrollValueWhenContentUpdated="True">
|
||||
<mdxaml:MarkdownScrollViewer.Styles>
|
||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
||||
</mdxaml:MarkdownScrollViewer.Styles>
|
||||
</mdxaml:MarkdownScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
<controls:SplitMarkdownEditor Grid.Row="1" Title="Description" Markdown="{CompiledBinding Description}"/>
|
||||
|
||||
<TextBlock Grid.Row="3"
|
||||
<TextBlock Grid.Row="2"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
|
||||
Margin="2 8 0 0"
|
||||
IsVisible="{CompiledBinding !DescriptionValid}">
|
||||
|
||||
@ -1,100 +1,11 @@
|
||||
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.Entries.Details;
|
||||
|
||||
public partial class EntrySpecificationsView : ReactiveUserControl<EntrySpecificationsViewModel>
|
||||
{
|
||||
private ScrollViewer? _editorScrollViewer;
|
||||
private ScrollViewer? _previewScrollViewer;
|
||||
private bool _updating;
|
||||
|
||||
public EntrySpecificationsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
DescriptionEditor.Options.AllowScrollBelowDocument = false;
|
||||
RegistryOptions options = new(ThemeName.Dark);
|
||||
TextMate.Installation? install = TextMate.InstallTextMate(DescriptionEditor, 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));
|
||||
}
|
||||
}
|
||||
@ -35,7 +35,6 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
|
||||
[Notify] private string _summary = string.Empty;
|
||||
[Notify] private string _description = string.Empty;
|
||||
[Notify] private Bitmap? _iconBitmap;
|
||||
[Notify] private TextDocument? _markdownDocument;
|
||||
[Notify(Setter.Private)] private bool _iconChanged;
|
||||
|
||||
public EntrySpecificationsViewModel(IWorkshopClient workshopClient, IWindowService windowService)
|
||||
@ -65,20 +64,7 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
|
||||
_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
|
||||
await PopulateCategories();
|
||||
|
||||
MarkdownDocument = new TextDocument(new StringTextSource(Description));
|
||||
MarkdownDocument.TextChanged += MarkdownDocumentOnTextChanged;
|
||||
Disposable.Create(() =>
|
||||
{
|
||||
MarkdownDocument.TextChanged -= MarkdownDocumentOnTextChanged;
|
||||
MarkdownDocument = null;
|
||||
ClearIcon();
|
||||
}).DisposeWith(d);
|
||||
});
|
||||
this.WhenActivatedAsync(async _ => await PopulateCategories());
|
||||
}
|
||||
|
||||
public ReactiveCommand<Unit, Unit> SelectIcon { get; }
|
||||
@ -92,12 +78,7 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
|
||||
public bool DescriptionValid => _descriptionValid.Value;
|
||||
|
||||
public List<long> PreselectedCategories { get; set; } = new();
|
||||
|
||||
private void MarkdownDocumentOnTextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
Description = MarkdownDocument?.Text ?? string.Empty;
|
||||
}
|
||||
|
||||
|
||||
private async Task ExecuteSelectIcon()
|
||||
{
|
||||
string[]? result = await _windowService.CreateOpenFileDialog()
|
||||
@ -112,12 +93,6 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
|
||||
IconChanged = true;
|
||||
}
|
||||
|
||||
private void ClearIcon()
|
||||
{
|
||||
IconBitmap?.Dispose();
|
||||
IconBitmap = null;
|
||||
}
|
||||
|
||||
private async Task PopulateCategories()
|
||||
{
|
||||
IOperationResult<IGetCategoriesResult> categories = await _workshopClient.GetCategories.ExecuteAsync();
|
||||
|
||||
@ -18,7 +18,7 @@ public partial class EntriesView : ReactiveUserControl<EntriesViewModel>
|
||||
|
||||
private void Navigate(ViewModelBase viewModel)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
|
||||
TabFrame.NavigateFromObject(viewModel);
|
||||
}
|
||||
|
||||
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
|
||||
|
||||
@ -53,8 +53,8 @@ public partial class EntriesViewModel : RoutableHostScreen<RoutableScreen>
|
||||
|
||||
public void GoBack()
|
||||
{
|
||||
if (ViewingDetails)
|
||||
_router.GoBack();
|
||||
if (ViewingDetails && SelectedTab != null)
|
||||
_router.Navigate(SelectedTab.Path);
|
||||
else
|
||||
_router.Navigate("workshop");
|
||||
}
|
||||
|
||||
@ -79,11 +79,11 @@
|
||||
<!-- Install state -->
|
||||
<StackPanel Grid.Column="2" Grid.Row="1" Margin="0 0 4 0" HorizontalAlignment="Right" VerticalAlignment="Bottom" IsVisible="{CompiledBinding IsInstalled}">
|
||||
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding !UpdateAvailable}">
|
||||
<avalonia:MaterialIcon Kind="CheckCircle" Foreground="{DynamicResource SystemAccentColorLight1}"/>
|
||||
<avalonia:MaterialIcon Kind="CheckCircle" Foreground="{DynamicResource SystemAccentColorLight1}" Width="20" Height="20"/>
|
||||
<Run>installed</Run>
|
||||
</TextBlock>
|
||||
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding UpdateAvailable}">
|
||||
<avalonia:MaterialIcon Kind="Update" Foreground="{DynamicResource SystemAccentColorLight1}"/>
|
||||
<avalonia:MaterialIcon Kind="Update" Foreground="{DynamicResource SystemAccentColorLight1}" Width="20" Height="20"/>
|
||||
<Run>update available</Run>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
<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:list="clr-namespace:Artemis.UI.Screens.Workshop.Entries.List"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Entries.List.EntryListView"
|
||||
x:DataType="list:EntryListViewModel">
|
||||
<UserControl.Styles>
|
||||
<Styles>
|
||||
<Style Selector="StackPanel.empty-state > TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Center"></Setter>
|
||||
<Setter Property="TextWrapping" Value="Wrap"></Setter>
|
||||
</Style>
|
||||
</Styles>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto">
|
||||
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top">
|
||||
<Border Classes="card" VerticalAlignment="Stretch">
|
||||
<StackPanel>
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
|
||||
<Border Classes="card-separator" />
|
||||
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
|
||||
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}" />
|
||||
|
||||
<ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged" Offset="{CompiledBinding ScrollOffset}">
|
||||
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<Panel Grid.Column="1" Grid.Row="1" IsVisible="{CompiledBinding !Initializing}">
|
||||
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like your current filters gave no results</TextBlock>
|
||||
<TextBlock>
|
||||
<Run>Modify or clear your filters to view other entries</Run>
|
||||
</TextBlock>
|
||||
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -1,38 +1,30 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Threading;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Avalonia.Threading;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.List;
|
||||
|
||||
public partial class LayoutListView : ReactiveUserControl<LayoutListViewModel>
|
||||
public partial class EntryListView : ReactiveUserControl<EntryListViewModel>
|
||||
{
|
||||
public LayoutListView()
|
||||
public EntryListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch();
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
UpdateEntriesPerFetch();
|
||||
ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d);
|
||||
});
|
||||
|
||||
this.WhenActivated(_ => UpdateEntriesPerFetch());
|
||||
}
|
||||
|
||||
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
if (ViewModel == null)
|
||||
return;
|
||||
|
||||
// When near the bottom of EntriesScrollViewer, call FetchMore on the view model
|
||||
if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100)
|
||||
ViewModel?.FetchMore(CancellationToken.None);
|
||||
}
|
||||
ViewModel.FetchMore(CancellationToken.None);
|
||||
|
||||
private void Navigate(RoutableScreen viewModel)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle);
|
||||
ViewModel.ScrollOffset = EntriesScrollViewer.Offset;
|
||||
}
|
||||
|
||||
private void UpdateEntriesPerFetch()
|
||||
@ -6,6 +6,7 @@ using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.UI.Extensions;
|
||||
using Artemis.UI.Screens.Workshop.Categories;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
@ -15,29 +16,28 @@ using DynamicData;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using ReactiveUI;
|
||||
using StrawberryShake;
|
||||
using Vector = Avalonia.Vector;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.List;
|
||||
|
||||
public abstract partial class EntryListViewModel : RoutableHostScreen<RoutableScreen>
|
||||
public partial class EntryListViewModel : RoutableScreen
|
||||
{
|
||||
private readonly SourceList<IEntrySummary> _entries = new();
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly IWorkshopClient _workshopClient;
|
||||
private readonly string _route;
|
||||
private IGetEntriesv2_EntriesV2_PageInfo? _currentPageInfo;
|
||||
|
||||
[Notify] private bool _initializing = true;
|
||||
[Notify] private bool _fetchingMore;
|
||||
[Notify] private int _entriesPerFetch;
|
||||
[Notify] private Vector _scrollOffset;
|
||||
|
||||
protected EntryListViewModel(string route,
|
||||
IWorkshopClient workshopClient,
|
||||
protected EntryListViewModel(IWorkshopClient workshopClient,
|
||||
CategoriesViewModel categoriesViewModel,
|
||||
EntryListInputViewModel entryListInputViewModel,
|
||||
INotificationService notificationService,
|
||||
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
|
||||
{
|
||||
_route = route;
|
||||
_workshopClient = workshopClient;
|
||||
_notificationService = notificationService;
|
||||
|
||||
@ -50,37 +50,31 @@ public abstract partial class EntryListViewModel : RoutableHostScreen<RoutableSc
|
||||
.Subscribe();
|
||||
Entries = entries;
|
||||
|
||||
// Respond to filter query input changes
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
// Respond to filter query input changes
|
||||
InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d);
|
||||
CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => Reset()).DisposeWith(d);
|
||||
});
|
||||
|
||||
// Load entries when the view model is first activated
|
||||
this.WhenActivatedAsync(async _ =>
|
||||
{
|
||||
if (_entries.Count == 0)
|
||||
{
|
||||
await Task.Delay(250);
|
||||
await FetchMore(CancellationToken.None);
|
||||
Initializing = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public CategoriesViewModel CategoriesViewModel { get; }
|
||||
public EntryListInputViewModel InputViewModel { get; }
|
||||
public EntryType? EntryType { get; set; }
|
||||
|
||||
public ReadOnlyObservableCollection<EntryListItemViewModel> Entries { get; }
|
||||
|
||||
public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_entries.Count == 0)
|
||||
{
|
||||
await Task.Delay(250, cancellationToken);
|
||||
await FetchMore(cancellationToken);
|
||||
Initializing = false;
|
||||
}
|
||||
}
|
||||
|
||||
public override Task OnClosing(NavigationArguments args)
|
||||
{
|
||||
// Clear search if not navigating to a child
|
||||
if (!args.Path.StartsWith(_route))
|
||||
InputViewModel.ClearLastSearch();
|
||||
return base.OnClosing(args);
|
||||
}
|
||||
|
||||
|
||||
public async Task FetchMore(CancellationToken cancellationToken)
|
||||
{
|
||||
if (FetchingMore || _currentPageInfo != null && !_currentPageInfo.HasNextPage)
|
||||
@ -119,12 +113,19 @@ public abstract partial class EntryListViewModel : RoutableHostScreen<RoutableSc
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual EntryFilterInput GetFilter()
|
||||
private EntryFilterInput GetFilter()
|
||||
{
|
||||
return new EntryFilterInput {And = CategoriesViewModel.CategoryFilters};
|
||||
return new EntryFilterInput
|
||||
{
|
||||
And =
|
||||
[
|
||||
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType}},
|
||||
..CategoriesViewModel.CategoryFilters ?? []
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual IReadOnlyList<EntrySortInput> GetSort()
|
||||
private IReadOnlyList<EntrySortInput> GetSort()
|
||||
{
|
||||
// Sort by created at
|
||||
if (InputViewModel.SortBy == 1)
|
||||
|
||||
@ -1,67 +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:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Tabs"
|
||||
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.Entries.Tabs.LayoutListView"
|
||||
x:DataType="tabs:LayoutListViewModel">
|
||||
<UserControl.Styles>
|
||||
<Styles>
|
||||
<Style Selector="StackPanel.empty-state > TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Center"></Setter>
|
||||
<Setter Property="TextWrapping" Value="Wrap"></Setter>
|
||||
</Style>
|
||||
</Styles>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Panel>
|
||||
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNull}}">
|
||||
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top">
|
||||
<Border Classes="card" VerticalAlignment="Stretch">
|
||||
<StackPanel>
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
|
||||
<Border Classes="card-separator" />
|
||||
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
|
||||
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}" />
|
||||
|
||||
<ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged">
|
||||
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<Panel Grid.Column="1" Grid.Row="1" IsVisible="{CompiledBinding !Initializing}">
|
||||
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like your current filters gave no results</TextBlock>
|
||||
<TextBlock>
|
||||
<Run>Modify or clear your filters to view other device layouts</Run>
|
||||
</TextBlock>
|
||||
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Grid>
|
||||
|
||||
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<controls:Frame.NavigationPageFactory>
|
||||
<ui:PageFactory />
|
||||
</controls:Frame.NavigationPageFactory>
|
||||
</controls:Frame>
|
||||
</Panel>
|
||||
</UserControl>
|
||||
@ -1,34 +0,0 @@
|
||||
using System;
|
||||
using Artemis.UI.Screens.Workshop.Categories;
|
||||
using Artemis.UI.Screens.Workshop.Entries.List;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.WebClient.Workshop;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
|
||||
|
||||
public class LayoutListViewModel : List.EntryListViewModel
|
||||
{
|
||||
public LayoutListViewModel(IWorkshopClient workshopClient,
|
||||
CategoriesViewModel categoriesViewModel,
|
||||
EntryListInputViewModel entryListInputViewModel,
|
||||
INotificationService notificationService,
|
||||
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
|
||||
: base("workshop/entries/layouts", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
|
||||
{
|
||||
entryListInputViewModel.SearchWatermark = "Search layouts";
|
||||
}
|
||||
|
||||
protected override EntryFilterInput GetFilter()
|
||||
{
|
||||
return new EntryFilterInput
|
||||
{
|
||||
And = new[]
|
||||
{
|
||||
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Layout}},
|
||||
new EntryFilterInput(){LatestReleaseId = new LongOperationFilterInput {Gt = 0}},
|
||||
base.GetFilter()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,67 +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:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Tabs"
|
||||
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.Entries.Tabs.PluginListView"
|
||||
x:DataType="tabs:PluginListViewModel">
|
||||
<UserControl.Styles>
|
||||
<Styles>
|
||||
<Style Selector="StackPanel.empty-state > TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Center"></Setter>
|
||||
<Setter Property="TextWrapping" Value="Wrap"></Setter>
|
||||
</Style>
|
||||
</Styles>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Panel>
|
||||
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNull}}">
|
||||
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top">
|
||||
<Border Classes="card" VerticalAlignment="Stretch">
|
||||
<StackPanel>
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
|
||||
<Border Classes="card-separator" />
|
||||
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
|
||||
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}" />
|
||||
|
||||
<ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged">
|
||||
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<Panel Grid.Column="1" Grid.Row="1" IsVisible="{CompiledBinding !Initializing}">
|
||||
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like your current filters gave no results</TextBlock>
|
||||
<TextBlock>
|
||||
<Run>Modify or clear your filters to view other plugins</Run>
|
||||
</TextBlock>
|
||||
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Grid>
|
||||
|
||||
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<controls:Frame.NavigationPageFactory>
|
||||
<ui:PageFactory />
|
||||
</controls:Frame.NavigationPageFactory>
|
||||
</controls:Frame>
|
||||
</Panel>
|
||||
</UserControl>
|
||||
@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Threading;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Avalonia.Threading;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
|
||||
|
||||
public partial class PluginListView : ReactiveUserControl<PluginListViewModel>
|
||||
{
|
||||
public PluginListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch();
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
UpdateEntriesPerFetch();
|
||||
ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
// When near the bottom of EntriesScrollViewer, call FetchMore on the view model
|
||||
if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100)
|
||||
ViewModel?.FetchMore(CancellationToken.None);
|
||||
}
|
||||
|
||||
private void Navigate(RoutableScreen viewModel)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
|
||||
private void UpdateEntriesPerFetch()
|
||||
{
|
||||
if (ViewModel != null)
|
||||
ViewModel.EntriesPerFetch = (int) (EntriesScrollViewer.Viewport.Height / 120);
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
using System;
|
||||
using Artemis.UI.Screens.Workshop.Categories;
|
||||
using Artemis.UI.Screens.Workshop.Entries.List;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.WebClient.Workshop;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
|
||||
|
||||
public class PluginListViewModel : EntryListViewModel
|
||||
{
|
||||
public PluginListViewModel(IWorkshopClient workshopClient,
|
||||
CategoriesViewModel categoriesViewModel,
|
||||
EntryListInputViewModel entryListInputViewModel,
|
||||
INotificationService notificationService,
|
||||
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
|
||||
: base("workshop/entries/plugins", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
|
||||
{
|
||||
entryListInputViewModel.SearchWatermark = "Search plugins";
|
||||
}
|
||||
|
||||
protected override EntryFilterInput GetFilter()
|
||||
{
|
||||
return new EntryFilterInput
|
||||
{
|
||||
And = new[]
|
||||
{
|
||||
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Plugin}},
|
||||
base.GetFilter()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,67 +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:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Tabs"
|
||||
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.Entries.Tabs.ProfileListView"
|
||||
x:DataType="tabs:ProfileListViewModel">
|
||||
<UserControl.Styles>
|
||||
<Styles>
|
||||
<Style Selector="StackPanel.empty-state > TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Center"></Setter>
|
||||
<Setter Property="TextWrapping" Value="Wrap"></Setter>
|
||||
</Style>
|
||||
</Styles>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Panel>
|
||||
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNull}}">
|
||||
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top">
|
||||
<Border Classes="card" VerticalAlignment="Stretch">
|
||||
<StackPanel>
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
|
||||
<Border Classes="card-separator" />
|
||||
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
|
||||
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}" />
|
||||
|
||||
<ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged">
|
||||
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<Panel Grid.Column="1" Grid.Row="1" IsVisible="{CompiledBinding !Initializing}">
|
||||
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like your current filters gave no results</TextBlock>
|
||||
<TextBlock>
|
||||
<Run>Modify or clear your filters to view some awesome profiles</Run>
|
||||
</TextBlock>
|
||||
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Grid>
|
||||
|
||||
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<controls:Frame.NavigationPageFactory>
|
||||
<ui:PageFactory />
|
||||
</controls:Frame.NavigationPageFactory>
|
||||
</controls:Frame>
|
||||
</Panel>
|
||||
</UserControl>
|
||||
@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Threading;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Avalonia.Threading;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
|
||||
|
||||
public partial class ProfileListView : ReactiveUserControl<ProfileListViewModel>
|
||||
{
|
||||
public ProfileListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch();
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
UpdateEntriesPerFetch();
|
||||
ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
// When near the bottom of EntriesScrollViewer, call FetchMore on the view model
|
||||
if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100)
|
||||
ViewModel?.FetchMore(CancellationToken.None);
|
||||
}
|
||||
|
||||
private void Navigate(RoutableScreen viewModel)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
|
||||
private void UpdateEntriesPerFetch()
|
||||
{
|
||||
if (ViewModel != null)
|
||||
ViewModel.EntriesPerFetch = (int) (EntriesScrollViewer.Viewport.Height / 120);
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
using System;
|
||||
using Artemis.UI.Screens.Workshop.Categories;
|
||||
using Artemis.UI.Screens.Workshop.Entries.List;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.WebClient.Workshop;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
|
||||
|
||||
public class ProfileListViewModel : List.EntryListViewModel
|
||||
{
|
||||
public ProfileListViewModel(IWorkshopClient workshopClient,
|
||||
CategoriesViewModel categoriesViewModel,
|
||||
EntryListInputViewModel entryListInputViewModel,
|
||||
INotificationService notificationService,
|
||||
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
|
||||
: base("workshop/entries/profiles", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
|
||||
{
|
||||
entryListInputViewModel.SearchWatermark = "Search profiles";
|
||||
}
|
||||
|
||||
protected override EntryFilterInput GetFilter()
|
||||
{
|
||||
return new EntryFilterInput
|
||||
{
|
||||
And = new[]
|
||||
{
|
||||
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Profile}},
|
||||
base.GetFilter()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
<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:entryReleases="clr-namespace:Artemis.UI.Screens.Workshop.EntryReleases"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.EntryReleases.EntryReleaseItemView"
|
||||
x:DataType="entryReleases:EntryReleaseItemViewModel">
|
||||
<UserControl.Resources>
|
||||
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||
</UserControl.Resources>
|
||||
<UserControl.Styles>
|
||||
<Style Selector="avalonia|MaterialIcon.status-icon">
|
||||
<Setter Property="Width" Value="20" />
|
||||
<Setter Property="Height" Value="20" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight1}" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" Margin="0 5">
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{CompiledBinding Release.Version}"></TextBlock>
|
||||
<TextBlock Classes="subtitle" ToolTip.Tip="{CompiledBinding Release.CreatedAt, Converter={StaticResource DateTimeConverter}}">
|
||||
<avalonia:MaterialIcon Kind="Calendar" />
|
||||
<Run>Created</Run>
|
||||
<Run Text="{CompiledBinding Release.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<avalonia:MaterialIcon Classes="status-icon" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" Kind="CheckCircle" ToolTip.Tip="Current version" IsVisible="{CompiledBinding IsCurrentVersion}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -0,0 +1,11 @@
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.EntryReleases;
|
||||
|
||||
public partial class EntryReleaseItemView : ReactiveUserControl<EntryReleaseItemViewModel>
|
||||
{
|
||||
public EntryReleaseItemView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.WebClient.Workshop.Models;
|
||||
using Artemis.WebClient.Workshop.Services;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.EntryReleases;
|
||||
|
||||
public partial class EntryReleaseItemViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly IWorkshopService _workshopService;
|
||||
private readonly IEntryDetails _entry;
|
||||
[Notify] private bool _isCurrentVersion;
|
||||
|
||||
public EntryReleaseItemViewModel(IWorkshopService workshopService, IEntryDetails entry, IRelease release)
|
||||
{
|
||||
_workshopService = workshopService;
|
||||
_entry = entry;
|
||||
|
||||
Release = release;
|
||||
UpdateIsCurrentVersion();
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
Observable.FromEventPattern<InstalledEntry>(x => _workshopService.OnInstalledEntrySaved += x, x => _workshopService.OnInstalledEntrySaved -= x)
|
||||
.Subscribe(_ => UpdateIsCurrentVersion())
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public IRelease Release { get; }
|
||||
|
||||
private void UpdateIsCurrentVersion()
|
||||
{
|
||||
IsCurrentVersion = _workshopService.GetInstalledEntry(_entry.Id)?.ReleaseId == Release.Id;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
<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:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
|
||||
xmlns:entryReleases="clr-namespace:Artemis.UI.Screens.Workshop.EntryReleases"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.EntryReleases.EntryReleaseView"
|
||||
x:DataType="entryReleases:EntryReleaseViewModel">
|
||||
<UserControl.Resources>
|
||||
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||
<sharedConverters:BytesToStringConverter x:Key="BytesToStringConverter" />
|
||||
</UserControl.Resources>
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Grid.info-container">
|
||||
<Setter Property="Margin" Value="10" />
|
||||
</Style>
|
||||
<Style Selector="avalonia|MaterialIcon.info-icon">
|
||||
<Setter Property="VerticalAlignment" Value="Top" />
|
||||
<Setter Property="Margin" Value="0 3 10 0" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.info-title">
|
||||
<Setter Property="Margin" Value="0 0 0 5" />
|
||||
<Setter Property="Opacity" Value="0.8" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.info-body">
|
||||
</Style>
|
||||
<Style Selector="TextBlock.info-link">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight3}" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.info-link:pointerover">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight1}" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto">
|
||||
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Button Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Classes="icon-button" Margin="0 0 5 0" Command="{CompiledBinding Close}">
|
||||
<avalonia:MaterialIcon Kind="ArrowBack" />
|
||||
</Button>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Theme="{StaticResource SubtitleTextBlockStyle}">Release info</TextBlock>
|
||||
<StackPanel Grid.Column="2">
|
||||
<!-- Install progress -->
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="5"
|
||||
IsVisible="{CompiledBinding InstallationInProgress}">
|
||||
<ProgressBar VerticalAlignment="Center"
|
||||
Width="300"
|
||||
Value="{CompiledBinding InstallProgress, FallbackValue=0}">
|
||||
</ProgressBar>
|
||||
<Button
|
||||
Classes="accent"
|
||||
Margin="15 0 0 0"
|
||||
Width="80"
|
||||
VerticalAlignment="Center"
|
||||
Command="{CompiledBinding Cancel}">
|
||||
Cancel
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Install button -->
|
||||
<Panel IsVisible="{CompiledBinding !InstallationInProgress}" HorizontalAlignment="Right">
|
||||
<Button IsVisible="{CompiledBinding !IsCurrentVersion}" Classes="accent" Width="80" Command="{CompiledBinding Install}">
|
||||
Install
|
||||
</Button>
|
||||
<Button IsVisible="{CompiledBinding IsCurrentVersion}" Classes="accent" Width="80" Command="{CompiledBinding Reinstall}">
|
||||
Re-install
|
||||
</Button>
|
||||
</Panel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Border Classes="card-separator" />
|
||||
<Grid Margin="-5 -10" ColumnDefinitions="*,*,*">
|
||||
<Grid Grid.Column="0" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Left">
|
||||
<avalonia:MaterialIcon Kind="TickNetworkOutline" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Version</TextBlock>
|
||||
<TextBlock Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Classes="info-body"
|
||||
Cursor="Hand"
|
||||
Text="{CompiledBinding Release.Version}" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Column="1" ColumnDefinitions="*,*" RowDefinitions="*,*,*" Classes="info-container" HorizontalAlignment="Center">
|
||||
<avalonia:MaterialIcon Kind="Calendar" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Release date</TextBlock>
|
||||
<TextBlock Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Classes="info-body"
|
||||
Text="{CompiledBinding Release.CreatedAt, Converter={StaticResource DateTimeConverter}}" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Column="2" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Right">
|
||||
<avalonia:MaterialIcon Kind="File" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">File size</TextBlock>
|
||||
<TextBlock Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Classes="info-body"
|
||||
Text="{CompiledBinding Release.DownloadSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="1" Classes="card">
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Row="0" Classes="h5 no-margin">Release notes</TextBlock>
|
||||
<Border Grid.Row="1" Classes="card-separator" />
|
||||
|
||||
<TextBlock Grid.Row="2" Classes="subtitle" IsVisible="{CompiledBinding Release.Changelog, Converter={x:Static StringConverters.IsNullOrEmpty}}">
|
||||
There are no release notes for this release.
|
||||
</TextBlock>
|
||||
<mdxaml:MarkdownScrollViewer Grid.Row="2"
|
||||
Markdown="{CompiledBinding Release.Changelog}"
|
||||
MarkdownStyleName="FluentAvalonia"
|
||||
IsVisible="{CompiledBinding Release.Changelog, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<mdxaml:MarkdownScrollViewer.Styles>
|
||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
||||
</mdxaml:MarkdownScrollViewer.Styles>
|
||||
</mdxaml:MarkdownScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -0,0 +1,14 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.EntryReleases;
|
||||
|
||||
public partial class EntryReleaseView : ReactiveUserControl<EntryReleaseViewModel>
|
||||
{
|
||||
public EntryReleaseView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
using System;
|
||||
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.UI.Shared.Utilities;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
||||
using Artemis.WebClient.Workshop.Services;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using StrawberryShake;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.EntryReleases;
|
||||
|
||||
public partial class EntryReleaseViewModel : RoutableScreen<ReleaseDetailParameters>
|
||||
{
|
||||
private readonly IWorkshopClient _client;
|
||||
private readonly IRouter _router;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly IWindowService _windowService;
|
||||
private readonly IWorkshopService _workshopService;
|
||||
private readonly EntryInstallationHandlerFactory _factory;
|
||||
private readonly Progress<StreamProgress> _progress = new();
|
||||
|
||||
[Notify] private IGetReleaseById_Release? _release;
|
||||
[Notify] private float _installProgress;
|
||||
[Notify] private bool _installationInProgress;
|
||||
[Notify] private bool _isCurrentVersion;
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
public EntryReleaseViewModel(IWorkshopClient client, IRouter router, INotificationService notificationService, IWindowService windowService, IWorkshopService workshopService,
|
||||
EntryInstallationHandlerFactory factory)
|
||||
{
|
||||
_client = client;
|
||||
_router = router;
|
||||
_notificationService = notificationService;
|
||||
_windowService = windowService;
|
||||
_workshopService = workshopService;
|
||||
_factory = factory;
|
||||
_progress.ProgressChanged += (_, f) => InstallProgress = f.ProgressPercentage;
|
||||
}
|
||||
|
||||
public async Task Close()
|
||||
{
|
||||
await _router.GoUp();
|
||||
}
|
||||
|
||||
public async Task Install()
|
||||
{
|
||||
if (Release == null)
|
||||
return;
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
InstallProgress = 0;
|
||||
InstallationInProgress = true;
|
||||
try
|
||||
{
|
||||
IEntryInstallationHandler handler = _factory.CreateHandler(Release.Entry.EntryType);
|
||||
EntryInstallResult result = await handler.InstallAsync(Release.Entry, Release, _progress, _cts.Token);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_notificationService.CreateNotification().WithTitle("Installation succeeded").WithSeverity(NotificationSeverity.Success).Show();
|
||||
IsCurrentVersion = true;
|
||||
InstallationInProgress = false;
|
||||
await Manage();
|
||||
}
|
||||
else if (!_cts.IsCancellationRequested)
|
||||
_notificationService.CreateNotification().WithTitle("Installation failed").WithMessage(result.Message).WithSeverity(NotificationSeverity.Error).Show();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
InstallationInProgress = false;
|
||||
_windowService.ShowExceptionDialog("Failed to install workshop entry", e);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Manage()
|
||||
{
|
||||
if (Release?.Entry.EntryType != EntryType.Profile)
|
||||
await _router.Navigate("../../manage");
|
||||
}
|
||||
|
||||
public async Task Reinstall()
|
||||
{
|
||||
if (await _windowService.ShowConfirmContentDialog("Reinstall entry", "Are you sure you want to reinstall this entry?"))
|
||||
await Install();
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
IsCurrentVersion = Release != null && _workshopService.GetInstalledEntry(Release.Entry.Id)?.ReleaseId == Release.Id;
|
||||
}
|
||||
|
||||
#region Overrides of RoutableScreen
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task OnClosing(NavigationArguments args)
|
||||
{
|
||||
if (!InstallationInProgress)
|
||||
return Task.CompletedTask;
|
||||
|
||||
args.Cancel();
|
||||
_notificationService.CreateNotification().WithMessage("Please wait for the installation to finish").Show();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<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:entryReleases="clr-namespace:Artemis.UI.Screens.Workshop.EntryReleases"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.EntryReleases.EntryReleasesView"
|
||||
x:DataType="entryReleases:EntryReleasesViewModel"><StackPanel>
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Releases</TextBlock>
|
||||
<Border Classes="card-separator" />
|
||||
<ListBox ItemsSource="{CompiledBinding Releases}" SelectedItem="{CompiledBinding SelectedRelease}"/>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -0,0 +1,12 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.EntryReleases;
|
||||
|
||||
public partial class EntryReleasesView : ReactiveUserControl<EntryReleasesViewModel>
|
||||
{
|
||||
public EntryReleasesView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
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.Extensions;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.EntryReleases;
|
||||
|
||||
public partial class EntryReleasesViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly IRouter _router;
|
||||
[Notify] private EntryReleaseItemViewModel? _selectedRelease;
|
||||
|
||||
public EntryReleasesViewModel(IEntryDetails entry, IRouter router, Func<IRelease, EntryReleaseItemViewModel> getEntryReleaseItemViewModel)
|
||||
{
|
||||
_router = router;
|
||||
|
||||
Entry = entry;
|
||||
Releases = Entry.Releases.OrderByDescending(r => r.CreatedAt).Take(5).Select(r => getEntryReleaseItemViewModel(r)).ToList();
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
router.CurrentPath.Subscribe(p =>
|
||||
SelectedRelease = p != null && p.StartsWith($"{Entry.GetEntryPath()}/releases") && float.TryParse(p.Split('/').Last(), out float releaseId)
|
||||
? Releases.FirstOrDefault(r => r.Release.Id == releaseId)
|
||||
: null)
|
||||
.DisposeWith(d);
|
||||
|
||||
this.WhenAnyValue(vm => vm.SelectedRelease)
|
||||
.WhereNotNull()
|
||||
.Subscribe(s => _router.Navigate($"{Entry.GetEntryPath()}/releases/{s.Release.Id}"))
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public IEntryDetails Entry { get; }
|
||||
public List<EntryReleaseItemViewModel> Releases { get; }
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<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:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
|
||||
xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDescriptionView"
|
||||
x:DataType="layout:LayoutDescriptionViewModel">
|
||||
<StackPanel Spacing="10">
|
||||
<Border Classes="card">
|
||||
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
|
||||
<mdxaml:MarkdownScrollViewer.Styles>
|
||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
||||
</mdxaml:MarkdownScrollViewer.Styles>
|
||||
</mdxaml:MarkdownScrollViewer>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -0,0 +1,14 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||
|
||||
public partial class LayoutDescriptionView : ReactiveUserControl<LayoutDescriptionViewModel>
|
||||
{
|
||||
public LayoutDescriptionView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||
|
||||
public partial class LayoutDescriptionViewModel : RoutableScreen
|
||||
{
|
||||
[Notify] private IEntryDetails? _entry;
|
||||
}
|
||||
@ -3,7 +3,8 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
|
||||
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
|
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:ui="clr-namespace:Artemis.UI"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView"
|
||||
x:DataType="layout:LayoutDetailsViewModel">
|
||||
@ -12,21 +13,17 @@
|
||||
<Border Classes="card" VerticalAlignment="Top">
|
||||
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
||||
</Border>
|
||||
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.LatestRelease, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count, FallbackValue=False}">
|
||||
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="1" Grid.Column="1">
|
||||
<StackPanel Margin="10 0" Spacing="10">
|
||||
<Border Classes="card">
|
||||
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
|
||||
<mdxaml:MarkdownScrollViewer.Styles>
|
||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
||||
</mdxaml:MarkdownScrollViewer.Styles>
|
||||
</mdxaml:MarkdownScrollViewer>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" Margin="10 0">
|
||||
<controls:Frame.NavigationPageFactory>
|
||||
<ui:PageFactory />
|
||||
</controls:Frame.NavigationPageFactory>
|
||||
</controls:Frame>
|
||||
</ScrollViewer>
|
||||
|
||||
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" />
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using Avalonia.ReactiveUI;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||
|
||||
@ -7,5 +10,9 @@ public partial class LayoutDetailsView : ReactiveUserControl<LayoutDetailsViewMo
|
||||
public LayoutDetailsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
|
||||
.WhereNotNull()
|
||||
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
|
||||
.DisposeWith(d));
|
||||
}
|
||||
}
|
||||
@ -1,87 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Screens.Workshop.Entries.Details;
|
||||
using Artemis.UI.Screens.Workshop.Layout.Dialogs;
|
||||
using Artemis.UI.Screens.Workshop.EntryReleases;
|
||||
using Artemis.UI.Screens.Workshop.Parameters;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.WebClient.Workshop.Models;
|
||||
using Artemis.WebClient.Workshop.Services;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using StrawberryShake;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||
|
||||
public partial class LayoutDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
|
||||
public partial class LayoutDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters>
|
||||
{
|
||||
private readonly IWorkshopClient _client;
|
||||
private readonly IDeviceService _deviceService;
|
||||
private readonly IWindowService _windowService;
|
||||
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
|
||||
private readonly LayoutDescriptionViewModel _layoutDescriptionViewModel;
|
||||
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
|
||||
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
|
||||
[Notify] private IEntryDetails? _entry;
|
||||
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
|
||||
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
|
||||
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
|
||||
|
||||
public LayoutDetailsViewModel(IWorkshopClient client,
|
||||
IDeviceService deviceService,
|
||||
IWindowService windowService,
|
||||
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
|
||||
LayoutDescriptionViewModel layoutDescriptionViewModel,
|
||||
EntryInfoViewModel entryInfoViewModel,
|
||||
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
|
||||
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
|
||||
{
|
||||
_client = client;
|
||||
_deviceService = deviceService;
|
||||
_windowService = windowService;
|
||||
_getEntryInfoViewModel = getEntryInfoViewModel;
|
||||
_layoutDescriptionViewModel = layoutDescriptionViewModel;
|
||||
_getEntryReleasesViewModel = getEntryReleasesViewModel;
|
||||
_getEntryImagesViewModel = getEntryImagesViewModel;
|
||||
|
||||
RecycleScreen = false;
|
||||
EntryInfoViewModel = entryInfoViewModel;
|
||||
}
|
||||
|
||||
public override RoutableScreen DefaultScreen => _layoutDescriptionViewModel;
|
||||
public EntryInfoViewModel EntryInfoViewModel { get; }
|
||||
|
||||
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
|
||||
{
|
||||
await GetEntry(parameters.EntryId, cancellationToken);
|
||||
if (Entry?.Id != parameters.EntryId)
|
||||
await GetEntry(parameters.EntryId, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
|
||||
{
|
||||
Task grace = Task.Delay(300, cancellationToken);
|
||||
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
|
||||
if (result.IsErrorResult())
|
||||
return;
|
||||
|
||||
// Let the UI settle to avoid lag when deep linking
|
||||
await grace;
|
||||
|
||||
Entry = result.Data?.Entry;
|
||||
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
|
||||
EntryInfoViewModel.SetEntry(Entry);
|
||||
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
|
||||
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
|
||||
|
||||
if (EntryReleasesViewModel != null)
|
||||
EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished;
|
||||
}
|
||||
|
||||
private async Task OnInstallationFinished(InstalledEntry installedEntry)
|
||||
{
|
||||
// Find compatible devices
|
||||
ArtemisLayout layout = new(Path.Combine(installedEntry.GetReleaseDirectory().FullName, "layout.xml"));
|
||||
List<ArtemisDevice> devices = _deviceService.Devices.Where(d => d.RgbDevice.DeviceInfo.DeviceType == layout.RgbLayout.Type).ToList();
|
||||
|
||||
// If any are found, offer to apply
|
||||
if (devices.Any())
|
||||
{
|
||||
await _windowService.CreateContentDialog()
|
||||
.WithTitle("Apply layout to devices")
|
||||
.WithViewModel(out DeviceSelectionDialogViewModel vm, devices, installedEntry)
|
||||
.WithCloseButtonText(null)
|
||||
.HavingPrimaryButton(b => b.WithText("Continue").WithCommand(vm.Apply))
|
||||
.ShowAsync();
|
||||
}
|
||||
_layoutDescriptionViewModel.Entry = Entry;
|
||||
}
|
||||
}
|
||||
16
src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml
Normal file
16
src/Artemis.UI/Screens/Workshop/Layout/LayoutListView.axaml
Normal file
@ -0,0 +1,16 @@
|
||||
<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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:ui="clr-namespace:Artemis.UI"
|
||||
xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutListView"
|
||||
x:DataType="layout:LayoutListViewModel">
|
||||
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0">
|
||||
<controls:Frame.NavigationPageFactory>
|
||||
<ui:PageFactory />
|
||||
</controls:Frame.NavigationPageFactory>
|
||||
</controls:Frame>
|
||||
</UserControl>
|
||||
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Avalonia.Threading;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||
|
||||
public partial class LayoutListView : ReactiveUserControl<LayoutListViewModel>
|
||||
{
|
||||
public LayoutListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
|
||||
.WhereNotNull()
|
||||
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
|
||||
.DisposeWith(d));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
using Artemis.UI.Screens.Workshop.Entries.List;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.WebClient.Workshop;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||
|
||||
public class LayoutListViewModel : RoutableHostScreen<RoutableScreen>
|
||||
{
|
||||
private readonly EntryListViewModel _entryListViewModel;
|
||||
public override RoutableScreen DefaultScreen => _entryListViewModel;
|
||||
|
||||
public LayoutListViewModel(EntryListViewModel entryListViewModel)
|
||||
{
|
||||
_entryListViewModel = entryListViewModel;
|
||||
_entryListViewModel.EntryType = EntryType.Layout;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
<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:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
|
||||
xmlns:surfaceEditor="clr-namespace:Artemis.UI.Screens.SurfaceEditor"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutManageView"
|
||||
x:DataType="layout:LayoutManageViewModel">
|
||||
<Border Classes="card" VerticalAlignment="Top">
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Button Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Classes="icon-button" Command="{CompiledBinding Close}">
|
||||
<avalonia:MaterialIcon Kind="ArrowBack" />
|
||||
</Button>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Theme="{StaticResource SubtitleTextBlockStyle}">Manage layout</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<Border Classes="card-separator" />
|
||||
|
||||
<TextBlock IsVisible="{CompiledBinding !Devices.Count}">
|
||||
This layout is made for devices of type
|
||||
<Run FontWeight="Bold" Text="{CompiledBinding Layout.RgbLayout.Type}"/>.<LineBreak/>
|
||||
Unfortunately, none were detected.
|
||||
</TextBlock>
|
||||
<StackPanel IsVisible="{CompiledBinding Devices.Count}">
|
||||
<TextBlock>
|
||||
Select the devices on which you would like to apply the downloaded layout.
|
||||
</TextBlock>
|
||||
<ItemsControl Name="EffectDescriptorsList" ItemsSource="{CompiledBinding Devices}" Margin="0 10">
|
||||
<ItemsControl.DataTemplates>
|
||||
<DataTemplate DataType="{x:Type surfaceEditor:ListDeviceViewModel}">
|
||||
<CheckBox IsChecked="{CompiledBinding IsSelected}">
|
||||
<TextBlock Text="{CompiledBinding Device.RgbDevice.DeviceInfo.DeviceName}"></TextBlock>
|
||||
</CheckBox>
|
||||
</DataTemplate>
|
||||
</ItemsControl.DataTemplates>
|
||||
</ItemsControl>
|
||||
|
||||
<Button Command="{CompiledBinding Apply}">Apply</Button>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@ -0,0 +1,14 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||
|
||||
public partial class LayoutManageView : ReactiveUserControl<LayoutManageViewModel>
|
||||
{
|
||||
public LayoutManageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
103
src/Artemis.UI/Screens/Workshop/Layout/LayoutManageViewModel.cs
Normal file
103
src/Artemis.UI/Screens/Workshop/Layout/LayoutManageViewModel.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.DryIoc.Factories;
|
||||
using Artemis.UI.Screens.SurfaceEditor;
|
||||
using Artemis.UI.Screens.Workshop.Parameters;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.WebClient.Workshop.Models;
|
||||
using Artemis.WebClient.Workshop.Providers;
|
||||
using Artemis.WebClient.Workshop.Services;
|
||||
using Avalonia.Threading;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||
|
||||
public partial class LayoutManageViewModel : RoutableScreen<WorkshopDetailParameters>
|
||||
{
|
||||
private readonly ISurfaceVmFactory _surfaceVmFactory;
|
||||
private readonly IRouter _router;
|
||||
private readonly IWorkshopService _workshopService;
|
||||
private readonly IDeviceService _deviceService;
|
||||
private readonly WorkshopLayoutProvider _layoutProvider;
|
||||
private readonly IWindowService _windowService;
|
||||
[Notify] private ArtemisLayout? _layout;
|
||||
[Notify] private InstalledEntry? _entry;
|
||||
[Notify] private ObservableCollection<ListDeviceViewModel>? _devices;
|
||||
|
||||
public LayoutManageViewModel(ISurfaceVmFactory surfaceVmFactory,
|
||||
IRouter router,
|
||||
IWorkshopService workshopService,
|
||||
IDeviceService deviceService,
|
||||
WorkshopLayoutProvider layoutProvider,
|
||||
IWindowService windowService)
|
||||
{
|
||||
_surfaceVmFactory = surfaceVmFactory;
|
||||
_router = router;
|
||||
_workshopService = workshopService;
|
||||
_deviceService = deviceService;
|
||||
_layoutProvider = layoutProvider;
|
||||
_windowService = windowService;
|
||||
Apply = ReactiveCommand.Create(ExecuteApply);
|
||||
ParameterSource = ParameterSource.Route;
|
||||
}
|
||||
|
||||
public ReactiveCommand<Unit, Unit> Apply { get; }
|
||||
|
||||
public async Task Close()
|
||||
{
|
||||
await _router.GoUp();
|
||||
}
|
||||
|
||||
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
|
||||
{
|
||||
InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(parameters.EntryId);
|
||||
if (installedEntry == null)
|
||||
{
|
||||
// TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
await _windowService.ShowConfirmContentDialog("Entry not found", "The entry you're trying to manage could not be found.", "Go back", null);
|
||||
await Close();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Layout = new ArtemisLayout(Path.Combine(installedEntry.GetReleaseDirectory().FullName, "layout.xml"));
|
||||
if (!Layout.IsValid)
|
||||
{
|
||||
// TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
await _windowService.ShowConfirmContentDialog("Invalid layout", "The layout of the entry you're trying to manage is invalid.", "Go back", null);
|
||||
await Close();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Entry = installedEntry;
|
||||
Devices = new ObservableCollection<ListDeviceViewModel>(_deviceService.Devices
|
||||
.Where(d => d.RgbDevice.DeviceInfo.DeviceType == Layout.RgbLayout.Type)
|
||||
.Select(_surfaceVmFactory.ListDeviceViewModel));
|
||||
}
|
||||
|
||||
private void ExecuteApply()
|
||||
{
|
||||
if (Devices == null)
|
||||
return;
|
||||
|
||||
foreach (ListDeviceViewModel listDeviceViewModel in Devices.Where(d => d.IsSelected))
|
||||
{
|
||||
_layoutProvider.ConfigureDevice(listDeviceViewModel.Device, Entry);
|
||||
_deviceService.SaveDevice(listDeviceViewModel.Device);
|
||||
_deviceService.LoadDeviceLayout(listDeviceViewModel.Device);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -1,11 +0,0 @@
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Library;
|
||||
|
||||
public partial class SubmissionDetailView : ReactiveUserControl<SubmissionDetailViewModel>
|
||||
{
|
||||
public SubmissionDetailView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
<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">
|
||||
<Border Classes="card" Grid.Column="0" Grid.Row="0">
|
||||
<ContentControl Content="{CompiledBinding EntrySpecificationsViewModel}"></ContentControl>
|
||||
</Border>
|
||||
|
||||
<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>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -91,6 +79,8 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
||||
bool confirmed = await _windowService.ShowConfirmContentDialog("You have unsaved changes", "Do you want to discard your unsaved changes?");
|
||||
if (!confirmed)
|
||||
args.Cancel();
|
||||
else
|
||||
await ExecuteDiscardChanges();
|
||||
}
|
||||
|
||||
private async Task ApplyDetailsFromEntry(CancellationToken cancellationToken)
|
||||
@ -106,6 +96,7 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
||||
if (Entry == null)
|
||||
{
|
||||
EntrySpecificationsViewModel = null;
|
||||
ApplyImagesFromEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -188,7 +179,6 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
||||
private async Task ExecuteDiscardChanges()
|
||||
{
|
||||
await ApplyDetailsFromEntry(CancellationToken.None);
|
||||
ApplyImagesFromEntry();
|
||||
}
|
||||
|
||||
private async Task ExecuteSaveChanges(CancellationToken cancellationToken)
|
||||
@ -243,30 +233,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 +264,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();
|
||||
@ -0,0 +1,85 @@
|
||||
<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>
|
||||
<Panel>
|
||||
<ProgressBar HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}"
|
||||
IsIndeterminate="True" />
|
||||
<Grid ColumnDefinitions="300,*" RowDefinitions="*, Auto" IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<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>
|
||||
</Panel>
|
||||
</UserControl>
|
||||
@ -0,0 +1,18 @@
|
||||
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)
|
||||
.WhereNotNull()
|
||||
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
|
||||
.DisposeWith(d));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
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;
|
||||
private readonly SubmissionDetailsViewModel _detailsViewModel;
|
||||
|
||||
[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;
|
||||
|
||||
RecycleScreen = false;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
this.WhenAnyValue(vm => vm.SelectedRelease)
|
||||
.WhereNotNull()
|
||||
.Subscribe(r => _router.Navigate($"workshop/library/submissions/{Entry?.Id}/releases/{r.Id}"))
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public override RoutableScreen DefaultScreen => _detailsViewModel;
|
||||
|
||||
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;
|
||||
|
||||
// OnNavigating may just be getting called to update the selected release
|
||||
if (Entry?.Id == parameters.EntryId)
|
||||
{
|
||||
// Reapply the entry when closing a release, this is mainly because the entry icon probably got disposed
|
||||
if (SelectedRelease == null)
|
||||
await _detailsViewModel.SetEntry(Entry, cancellationToken);
|
||||
|
||||
// No need to reload the entry since it's the same
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
<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:controls1="clr-namespace:Artemis.UI.Controls"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Library.SubmissionReleaseView"
|
||||
x:DataType="library:SubmissionReleaseViewModel">
|
||||
<UserControl.Resources>
|
||||
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||
<sharedConverters:BytesToStringConverter x:Key="BytesToStringConverter" />
|
||||
</UserControl.Resources>
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Grid.info-container">
|
||||
<Setter Property="Margin" Value="10" />
|
||||
</Style>
|
||||
<Style Selector="avalonia|MaterialIcon.info-icon">
|
||||
<Setter Property="VerticalAlignment" Value="Top" />
|
||||
<Setter Property="Margin" Value="0 3 10 0" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.info-title">
|
||||
<Setter Property="Margin" Value="0 0 0 5" />
|
||||
<Setter Property="Opacity" Value="0.8" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.info-body">
|
||||
</Style>
|
||||
<Style Selector="TextBlock.info-link">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight3}" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.info-link:pointerover">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight1}" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Button Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Classes="icon-button" Command="{CompiledBinding Close}" Margin="0 0 5 0">
|
||||
<avalonia:MaterialIcon Kind="ArrowBack" />
|
||||
</Button>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Theme="{StaticResource SubtitleTextBlockStyle}">Release management</TextBlock>
|
||||
<Button Grid.Row="0" Grid.Column="2" HorizontalAlignment="Right" Classes="danger" Command="{CompiledBinding DeleteRelease}">Delete release</Button>
|
||||
</Grid>
|
||||
<Border Classes="card-separator" />
|
||||
<Grid Margin="-5 -10" ColumnDefinitions="*,*,*">
|
||||
<Grid Grid.Column="0" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Left">
|
||||
<avalonia:MaterialIcon Kind="TickNetworkOutline" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Version</TextBlock>
|
||||
<TextBlock Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Classes="info-body"
|
||||
Cursor="Hand"
|
||||
Text="{CompiledBinding Release.Version}" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Column="1" ColumnDefinitions="*,*" RowDefinitions="*,*,*" Classes="info-container" HorizontalAlignment="Center">
|
||||
<avalonia:MaterialIcon Kind="Calendar" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Release date</TextBlock>
|
||||
<TextBlock Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Classes="info-body"
|
||||
Text="{CompiledBinding Release.CreatedAt, Converter={StaticResource DateTimeConverter}}" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Column="2" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Right">
|
||||
<avalonia:MaterialIcon Kind="File" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">File size</TextBlock>
|
||||
<TextBlock Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Classes="info-body"
|
||||
Text="{CompiledBinding Release.DownloadSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="1" Classes="card">
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Row="0" Classes="h5 no-margin">Release notes</TextBlock>
|
||||
<Border Grid.Row="1" Classes="card-separator" />
|
||||
|
||||
<controls1:SplitMarkdownEditor Grid.Row="2" Title="Changelog" Markdown="{CompiledBinding Changelog}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="2" Margin="0 10 0 0" Orientation="Horizontal" Spacing="5" HorizontalAlignment="Right">
|
||||
<Button Command="{CompiledBinding Discard}">Discard changes</Button>
|
||||
<Button Command="{CompiledBinding Save}">Save</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -0,0 +1,11 @@
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Library;
|
||||
|
||||
public partial class SubmissionReleaseView : ReactiveUserControl<SubmissionReleaseViewModel>
|
||||
{
|
||||
public SubmissionReleaseView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
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;
|
||||
|
||||
[Notify] private IGetReleaseById_Release? _release;
|
||||
[Notify] private string? _changelog;
|
||||
[Notify] private bool _hasChanges;
|
||||
|
||||
public SubmissionReleaseViewModel(IWorkshopClient client, IRouter router, IWindowService windowService, INotificationService notificationService)
|
||||
{
|
||||
_client = client;
|
||||
_router = router;
|
||||
_windowService = windowService;
|
||||
_notificationService = notificationService;
|
||||
this.WhenAnyValue(vm => vm.Changelog, vm => vm.Release, (current, release) => current != release?.Changelog).Subscribe(hasChanges => HasChanges = hasChanges);
|
||||
|
||||
Discard = ReactiveCommand.Create(ExecuteDiscard, this.WhenAnyValue(vm => vm.HasChanges));
|
||||
Save = ReactiveCommand.CreateFromTask(ExecuteSave, this.WhenAnyValue(vm => vm.HasChanges));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
HasChanges = false;
|
||||
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();
|
||||
|
||||
HasChanges = false;
|
||||
}
|
||||
|
||||
private void ExecuteDiscard()
|
||||
{
|
||||
Changelog = Release?.Changelog;
|
||||
HasChanges = false;
|
||||
}
|
||||
}
|
||||
@ -18,9 +18,9 @@ public partial class WorkshopLibraryView : ReactiveUserControl<WorkshopLibraryVi
|
||||
|
||||
private void Navigate(ViewModelBase viewModel)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
|
||||
TabFrame.NavigateFromObject(viewModel);
|
||||
}
|
||||
|
||||
|
||||
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
|
||||
{
|
||||
ViewModel?.GoBack();
|
||||
|
||||
@ -53,7 +53,7 @@ public partial class WorkshopLibraryViewModel : RoutableHostScreen<RoutableScree
|
||||
public void GoBack()
|
||||
{
|
||||
if (ViewingDetails)
|
||||
_router.GoBack();
|
||||
_router.Navigate("workshop/library/submissions");
|
||||
else
|
||||
_router.Navigate("workshop");
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
namespace Artemis.UI.Screens.Workshop.Parameters;
|
||||
|
||||
public class ReleaseDetailParameters
|
||||
{
|
||||
public long ReleaseId { get; set; }
|
||||
}
|
||||
@ -1,19 +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:dialogs="clr-namespace:Artemis.UI.Screens.Workshop.Plugins.Dialogs"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Plugins.Dialogs.PluginDialogView"
|
||||
x:DataType="dialogs:PluginDialogViewModel">
|
||||
<Grid ColumnDefinitions="4*,5*" Width="800" Height="160">
|
||||
<ContentControl Grid.Column="0" Content="{CompiledBinding PluginViewModel}" />
|
||||
|
||||
<Border Grid.Column="1" BorderBrush="{DynamicResource ButtonBorderBrush}" BorderThickness="1 0 0 0" Margin="10 0 0 0" Padding="10 0 0 0">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<TextBlock Classes="h5">Plugin features</TextBlock>
|
||||
<ListBox Grid.Row="1" MaxHeight="135" ItemsSource="{CompiledBinding PluginFeatures}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -1,11 +0,0 @@
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Plugins.Dialogs;
|
||||
|
||||
public partial class PluginDialogView : ReactiveUserControl<PluginDialogViewModel>
|
||||
{
|
||||
public PluginDialogView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.DryIoc.Factories;
|
||||
using Artemis.UI.Screens.Plugins;
|
||||
using Artemis.UI.Screens.Plugins.Features;
|
||||
using Artemis.UI.Shared;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Plugins.Dialogs;
|
||||
|
||||
public class PluginDialogViewModel : ContentDialogViewModelBase
|
||||
{
|
||||
public PluginDialogViewModel(Plugin plugin, ISettingsVmFactory settingsVmFactory)
|
||||
{
|
||||
PluginViewModel = settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => {}, Observable.Empty<bool>()));
|
||||
PluginFeatures = new ObservableCollection<PluginFeatureViewModel>(plugin.Features.Select(f => settingsVmFactory.PluginFeatureViewModel(f, false)));
|
||||
}
|
||||
|
||||
public PluginViewModel PluginViewModel { get; }
|
||||
public ObservableCollection<PluginFeatureViewModel> PluginFeatures { get; }
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
<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:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
|
||||
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginDescriptionView"
|
||||
x:DataType="plugins:PluginDescriptionViewModel">
|
||||
<StackPanel Spacing="10">
|
||||
<Border Classes="card">
|
||||
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
|
||||
<mdxaml:MarkdownScrollViewer.Styles>
|
||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
||||
</mdxaml:MarkdownScrollViewer.Styles>
|
||||
</mdxaml:MarkdownScrollViewer>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Dependants, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<StackPanel>
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Used by these profiles</TextBlock>
|
||||
<Border Classes="card-separator" />
|
||||
<ScrollViewer>
|
||||
<ItemsControl ItemsSource="{CompiledBinding Dependants}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="5"></StackPanel>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -0,0 +1,14 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Plugins;
|
||||
|
||||
public partial class PluginDescriptionView : ReactiveUserControl<PluginDescriptionViewModel>
|
||||
{
|
||||
public PluginDescriptionView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.UI.Screens.Workshop.Entries.List;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Plugins;
|
||||
|
||||
public partial class PluginDescriptionViewModel : RoutableScreen
|
||||
{
|
||||
[Notify] private IEntryDetails? _entry;
|
||||
[Notify] private List<EntryListItemViewModel>? _dependants;
|
||||
private readonly IWorkshopClient _client;
|
||||
private readonly Func<IEntrySummary, EntryListItemViewModel> _getEntryListViewModel;
|
||||
|
||||
public PluginDescriptionViewModel(IWorkshopClient client, Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
|
||||
{
|
||||
_client = client;
|
||||
_getEntryListViewModel = getEntryListViewModel;
|
||||
}
|
||||
|
||||
public async Task SetEntry(IEntryDetails? entry, CancellationToken cancellationToken)
|
||||
{
|
||||
Entry = entry;
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
IReadOnlyList<IEntrySummary>? dependants = (await _client.GetDependantEntries.ExecuteAsync(entry.Id, 0, 25, cancellationToken)).Data?.Entries?.Items;
|
||||
Dependants = dependants != null && dependants.Any() ? dependants.Select(_getEntryListViewModel).OrderByDescending(d => d.Entry.Downloads).Take(10).ToList() : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Dependants = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,9 +2,10 @@
|
||||
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:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
|
||||
xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
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.Plugins.PluginDetailsView"
|
||||
x:DataType="plugins:PluginDetailsViewModel">
|
||||
@ -15,47 +16,44 @@
|
||||
</Border>
|
||||
|
||||
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.PluginInfo, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<StackPanel>
|
||||
<TextBlock>Admin required</TextBlock>
|
||||
<TextBlock Text="Yes" IsVisible="{CompiledBinding Entry.PluginInfo.RequiresAdmin}" />
|
||||
<TextBlock Text="No" IsVisible="{CompiledBinding !Entry.PluginInfo.RequiresAdmin}" />
|
||||
|
||||
<TextBlock Margin="0 15 0 5">Supported platforms</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<avalonia:MaterialIcon Kind="MicrosoftWindows" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsWindows}" />
|
||||
<avalonia:MaterialIcon Kind="Linux" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsLinux}" />
|
||||
<avalonia:MaterialIcon Kind="Apple" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsOSX}" />
|
||||
<Panel>
|
||||
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}">
|
||||
<Border Width="110" Classes="skeleton-text"></Border>
|
||||
<Border Width="35" Classes="skeleton-text"></Border>
|
||||
|
||||
<Border Margin="0 16 0 3" Width="130" Classes="skeleton-text"></Border>
|
||||
<Border Width="60" Classes="skeleton-text"></Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<TextBlock>Admin required</TextBlock>
|
||||
<TextBlock Text="Yes" IsVisible="{CompiledBinding Entry.PluginInfo.RequiresAdmin}" />
|
||||
<TextBlock Text="No" IsVisible="{CompiledBinding !Entry.PluginInfo.RequiresAdmin}" />
|
||||
|
||||
<TextBlock Margin="0 15 0 5">Supported platforms</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<avalonia:MaterialIcon Kind="MicrosoftWindows" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsWindows}" />
|
||||
<avalonia:MaterialIcon Kind="Linux" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsLinux}" />
|
||||
<avalonia:MaterialIcon Kind="Apple" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsOSX}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
</Border>
|
||||
|
||||
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.LatestRelease, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count, FallbackValue=False}">
|
||||
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="1" Grid.Column="1">
|
||||
<StackPanel Margin="10 0" Spacing="10">
|
||||
<Border Classes="card">
|
||||
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
|
||||
<mdxaml:MarkdownScrollViewer.Styles>
|
||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
||||
</mdxaml:MarkdownScrollViewer.Styles>
|
||||
</mdxaml:MarkdownScrollViewer>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Dependants, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<StackPanel>
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Used by these profiles</TextBlock>
|
||||
<Border Classes="card-separator" />
|
||||
<ScrollViewer>
|
||||
<ItemsControl ItemsSource="{CompiledBinding Dependants}"></ItemsControl>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" Margin="10 0">
|
||||
<controls:Frame.NavigationPageFactory>
|
||||
<ui:PageFactory />
|
||||
</controls:Frame.NavigationPageFactory>
|
||||
</controls:Frame>
|
||||
</ScrollViewer>
|
||||
|
||||
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" />
|
||||
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count, FallbackValue=False}" Content="{CompiledBinding EntryImagesViewModel}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using Avalonia.ReactiveUI;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Plugins;
|
||||
|
||||
@ -7,5 +10,9 @@ public partial class PluginDetailsView : ReactiveUserControl<PluginDetailsViewMo
|
||||
public PluginDetailsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
|
||||
.WhereNotNull()
|
||||
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
|
||||
.DisposeWith(d));
|
||||
}
|
||||
}
|
||||
@ -1,104 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Screens.Workshop.Entries.Details;
|
||||
using Artemis.UI.Screens.Workshop.Entries.List;
|
||||
using Artemis.UI.Screens.Workshop.EntryReleases;
|
||||
using Artemis.UI.Screens.Workshop.Parameters;
|
||||
using Artemis.UI.Screens.Workshop.Plugins.Dialogs;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.WebClient.Workshop.Models;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using StrawberryShake;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Plugins;
|
||||
|
||||
public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
|
||||
public partial class PluginDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters>
|
||||
{
|
||||
private readonly IWorkshopClient _client;
|
||||
private readonly IWindowService _windowService;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
|
||||
private readonly PluginDescriptionViewModel _pluginDescriptionViewModel;
|
||||
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
|
||||
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
|
||||
private readonly Func<IEntrySummary, EntryListItemViewModel> _getEntryListViewModel;
|
||||
[Notify] private IGetPluginEntryById_Entry? _entry;
|
||||
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
|
||||
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
|
||||
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
|
||||
[Notify] private ReadOnlyObservableCollection<EntryListItemViewModel>? _dependants;
|
||||
|
||||
|
||||
public PluginDetailsViewModel(IWorkshopClient client,
|
||||
IWindowService windowService,
|
||||
IPluginManagementService pluginManagementService,
|
||||
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
|
||||
PluginDescriptionViewModel pluginDescriptionViewModel,
|
||||
EntryInfoViewModel entryInfoViewModel,
|
||||
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
|
||||
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel,
|
||||
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
|
||||
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
|
||||
{
|
||||
_client = client;
|
||||
_windowService = windowService;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
_getEntryInfoViewModel = getEntryInfoViewModel;
|
||||
_pluginDescriptionViewModel = pluginDescriptionViewModel;
|
||||
_getEntryReleasesViewModel = getEntryReleasesViewModel;
|
||||
_getEntryImagesViewModel = getEntryImagesViewModel;
|
||||
_getEntryListViewModel = getEntryListViewModel;
|
||||
|
||||
EntryInfoViewModel = entryInfoViewModel;
|
||||
RecycleScreen = false;
|
||||
}
|
||||
|
||||
public override RoutableScreen DefaultScreen => _pluginDescriptionViewModel;
|
||||
public EntryInfoViewModel EntryInfoViewModel { get; }
|
||||
|
||||
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
|
||||
{
|
||||
await GetEntry(parameters.EntryId, cancellationToken);
|
||||
if (Entry?.Id != parameters.EntryId)
|
||||
await GetEntry(parameters.EntryId, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
|
||||
{
|
||||
Task grace = Task.Delay(300, cancellationToken);
|
||||
IOperationResult<IGetPluginEntryByIdResult> result = await _client.GetPluginEntryById.ExecuteAsync(entryId, cancellationToken);
|
||||
if (result.IsErrorResult())
|
||||
return;
|
||||
|
||||
// Let the UI settle to avoid lag when deep linking
|
||||
await grace;
|
||||
|
||||
Entry = result.Data?.Entry;
|
||||
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
|
||||
EntryInfoViewModel.SetEntry(Entry);
|
||||
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
|
||||
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
|
||||
|
||||
if (EntryReleasesViewModel != null)
|
||||
{
|
||||
EntryReleasesViewModel.OnInstallationStarted = OnInstallationStarted;
|
||||
EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished;
|
||||
}
|
||||
|
||||
IReadOnlyList<IEntrySummary>? dependants = (await _client.GetDependantEntries.ExecuteAsync(entryId, 0, 25, cancellationToken)).Data?.Entries?.Items;
|
||||
Dependants = dependants != null && dependants.Any()
|
||||
? new ReadOnlyObservableCollection<EntryListItemViewModel>(new ObservableCollection<EntryListItemViewModel>(dependants.Select(_getEntryListViewModel)))
|
||||
: null;
|
||||
}
|
||||
|
||||
private async Task<bool> OnInstallationStarted(IEntryDetails entryDetails)
|
||||
{
|
||||
bool confirm = await _windowService.ShowConfirmContentDialog(
|
||||
"Installing plugin",
|
||||
$"You are about to install version {entryDetails.LatestRelease?.Version} of {entryDetails.Name}. \r\n\r\n" +
|
||||
"Plugins are NOT verified by Artemis and could harm your PC, if you have doubts about a plugin please ask on Discord!",
|
||||
"I trust this plugin, install it"
|
||||
);
|
||||
|
||||
return !confirm;
|
||||
}
|
||||
|
||||
private async Task OnInstallationFinished(InstalledEntry installedEntry)
|
||||
{
|
||||
if (!installedEntry.TryGetMetadata("PluginId", out Guid pluginId))
|
||||
return;
|
||||
Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
|
||||
if (plugin == null)
|
||||
return;
|
||||
|
||||
await _windowService.CreateContentDialog().WithTitle("Manage plugin").WithViewModel(out PluginDialogViewModel _, plugin).WithFullScreen().ShowAsync();
|
||||
await _pluginDescriptionViewModel.SetEntry(Entry, cancellationToken);
|
||||
}
|
||||
}
|
||||
16
src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml
Normal file
16
src/Artemis.UI/Screens/Workshop/Plugins/PluginListView.axaml
Normal file
@ -0,0 +1,16 @@
|
||||
<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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:ui="clr-namespace:Artemis.UI"
|
||||
xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginListView"
|
||||
x:DataType="plugins:PluginListViewModel">
|
||||
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0">
|
||||
<controls:Frame.NavigationPageFactory>
|
||||
<ui:PageFactory />
|
||||
</controls:Frame.NavigationPageFactory>
|
||||
</controls:Frame>
|
||||
</UserControl>
|
||||
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using Avalonia.ReactiveUI;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Plugins;
|
||||
|
||||
public partial class PluginListView : ReactiveUserControl<PluginListViewModel>
|
||||
{
|
||||
public PluginListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
|
||||
.WhereNotNull()
|
||||
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
|
||||
.DisposeWith(d));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
using Artemis.UI.Screens.Workshop.Entries.List;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.WebClient.Workshop;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Plugins;
|
||||
|
||||
public class PluginListViewModel : RoutableHostScreen<RoutableScreen>
|
||||
{
|
||||
private readonly EntryListViewModel _entryListViewModel;
|
||||
public override RoutableScreen DefaultScreen => _entryListViewModel;
|
||||
|
||||
public PluginListViewModel(EntryListViewModel entryListViewModel)
|
||||
{
|
||||
_entryListViewModel = entryListViewModel;
|
||||
_entryListViewModel.EntryType = EntryType.Plugin;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<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:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginManageView"
|
||||
x:DataType="plugins:PluginManageViewModel">
|
||||
<Border Classes="card" VerticalAlignment="Top">
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Button Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Classes="icon-button" Command="{CompiledBinding Close}">
|
||||
<avalonia:MaterialIcon Kind="ArrowBack" />
|
||||
</Button>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Theme="{StaticResource SubtitleTextBlockStyle}">Manage plugin</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<Border Classes="card-separator" />
|
||||
|
||||
<Grid ColumnDefinitions="4*,5*" Height="160">
|
||||
<ContentControl Grid.Column="0" Content="{CompiledBinding PluginViewModel}" />
|
||||
|
||||
<Border Grid.Column="1" BorderBrush="{DynamicResource ButtonBorderBrush}" BorderThickness="1 0 0 0" Margin="10 0 0 0" Padding="10 0 0 0">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<TextBlock Classes="h5">Plugin features</TextBlock>
|
||||
<ListBox Grid.Row="1" MaxHeight="135" ItemsSource="{CompiledBinding PluginFeatures}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@ -0,0 +1,14 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Plugins;
|
||||
|
||||
public partial class PluginManageView : ReactiveUserControl<PluginManageViewModel>
|
||||
{
|
||||
public PluginManageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.DryIoc.Factories;
|
||||
using Artemis.UI.Screens.Plugins;
|
||||
using Artemis.UI.Screens.Plugins.Features;
|
||||
using Artemis.UI.Screens.Workshop.Parameters;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.WebClient.Workshop.Models;
|
||||
using Artemis.WebClient.Workshop.Services;
|
||||
using Avalonia.Threading;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Plugins;
|
||||
|
||||
public partial class PluginManageViewModel : RoutableScreen<WorkshopDetailParameters>
|
||||
{
|
||||
private readonly ISettingsVmFactory _settingsVmFactory;
|
||||
private readonly IRouter _router;
|
||||
private readonly IWorkshopService _workshopService;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private readonly IWindowService _windowService;
|
||||
[Notify] private PluginViewModel? _pluginViewModel;
|
||||
[Notify] private ObservableCollection<PluginFeatureViewModel>? _pluginFeatures;
|
||||
|
||||
public PluginManageViewModel(ISettingsVmFactory settingsVmFactory, IRouter router, IWorkshopService workshopService, IPluginManagementService pluginManagementService, IWindowService windowService)
|
||||
{
|
||||
_settingsVmFactory = settingsVmFactory;
|
||||
_router = router;
|
||||
_workshopService = workshopService;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
_windowService = windowService;
|
||||
ParameterSource = ParameterSource.Route;
|
||||
}
|
||||
|
||||
public async Task Close()
|
||||
{
|
||||
await _router.GoUp();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
|
||||
{
|
||||
InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(parameters.EntryId);
|
||||
if (installedEntry == null || !installedEntry.TryGetMetadata("PluginId", out Guid pluginId))
|
||||
{
|
||||
// TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
await _windowService.ShowConfirmContentDialog("Invalid plugin", "The plugin you're trying to manage is invalid or doesn't exist", "Go back", null);
|
||||
await Close();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
|
||||
if (plugin == null)
|
||||
{
|
||||
// TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
await _windowService.ShowConfirmContentDialog("Invalid plugin", "The plugin you're trying to manage is invalid or doesn't exist", "Go back", null);
|
||||
await Close();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
PluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { }));
|
||||
PluginFeatures = new ObservableCollection<PluginFeatureViewModel>(plugin.Features.Select(f => _settingsVmFactory.PluginFeatureViewModel(f, false)));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
<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:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
|
||||
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDescriptionView"
|
||||
x:DataType="profile:ProfileDescriptionViewModel">
|
||||
<StackPanel Spacing="10">
|
||||
<Border Classes="card">
|
||||
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia" Name="MarkdownScrollViewer" >
|
||||
<mdxaml:MarkdownScrollViewer.Styles>
|
||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
||||
</mdxaml:MarkdownScrollViewer.Styles>
|
||||
</mdxaml:MarkdownScrollViewer>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Dependencies, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<StackPanel>
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Required plugins</TextBlock>
|
||||
<Border Classes="card-separator" />
|
||||
<ScrollViewer>
|
||||
<ItemsControl ItemsSource="{CompiledBinding Dependencies}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="5"></StackPanel>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -0,0 +1,14 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Profile;
|
||||
|
||||
public partial class ProfileDescriptionView : ReactiveUserControl<ProfileDescriptionViewModel>
|
||||
{
|
||||
public ProfileDescriptionView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.UI.Screens.Workshop.Entries.List;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Profile;
|
||||
|
||||
public partial class ProfileDescriptionViewModel : RoutableScreen
|
||||
{
|
||||
private readonly IWorkshopClient _client;
|
||||
private readonly Func<IEntrySummary, EntryListItemViewModel> _getEntryListViewModel;
|
||||
[Notify] private IEntryDetails? _entry;
|
||||
[Notify] private List<EntryListItemViewModel>? _dependencies;
|
||||
|
||||
public ProfileDescriptionViewModel(IWorkshopClient client, Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
|
||||
{
|
||||
_client = client;
|
||||
_getEntryListViewModel = getEntryListViewModel;
|
||||
}
|
||||
|
||||
public async Task SetEntry(IEntryDetails? entry, CancellationToken cancellationToken)
|
||||
{
|
||||
Entry = entry;
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
IReadOnlyList<IEntrySummary>? dependencies = (await _client.GetLatestDependencies.ExecuteAsync(entry.Id, cancellationToken)).Data?.Entry?.LatestRelease?.Dependencies;
|
||||
Dependencies = dependencies != null && dependencies.Any() ? dependencies.Select(_getEntryListViewModel).ToList() : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Dependencies = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,8 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
|
||||
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
|
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:ui="clr-namespace:Artemis.UI"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
||||
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView"
|
||||
x:DataType="profile:ProfileDetailsViewModel">
|
||||
@ -12,34 +13,19 @@
|
||||
<Border Classes="card" VerticalAlignment="Top">
|
||||
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
||||
</Border>
|
||||
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.LatestRelease, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count, FallbackValue=False}">
|
||||
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="1" Grid.Column="1">
|
||||
<StackPanel Margin="10 0" Spacing="10">
|
||||
<Border Classes="card">
|
||||
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
|
||||
<mdxaml:MarkdownScrollViewer.Styles>
|
||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
||||
</mdxaml:MarkdownScrollViewer.Styles>
|
||||
</mdxaml:MarkdownScrollViewer>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Dependencies, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<StackPanel>
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Required plugins</TextBlock>
|
||||
<Border Classes="card-separator" />
|
||||
<ScrollViewer>
|
||||
<ItemsControl ItemsSource="{CompiledBinding Dependencies}"></ItemsControl>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" Margin="10 0">
|
||||
<controls:Frame.NavigationPageFactory>
|
||||
<ui:PageFactory />
|
||||
</controls:Frame.NavigationPageFactory>
|
||||
</controls:Frame>
|
||||
</ScrollViewer>
|
||||
|
||||
|
||||
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" />
|
||||
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count, FallbackValue=False}" Content="{CompiledBinding EntryImagesViewModel}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using Avalonia.ReactiveUI;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Profile;
|
||||
|
||||
@ -7,5 +10,9 @@ public partial class ProfileDetailsView : ReactiveUserControl<ProfileDetailsView
|
||||
public ProfileDetailsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
|
||||
.WhereNotNull()
|
||||
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
|
||||
.DisposeWith(d));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user