mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge pull request #850 from Artemis-RGB/feature/releases
Workshop - Added release details and reworked installation management
This commit is contained in:
commit
c37c031dc2
@ -9,5 +9,6 @@ internal interface IRoutableHostScreen : IRoutableScreen
|
|||||||
{
|
{
|
||||||
bool RecycleScreen { get; }
|
bool RecycleScreen { get; }
|
||||||
IRoutableScreen? InternalScreen { get; }
|
IRoutableScreen? InternalScreen { get; }
|
||||||
|
IRoutableScreen? InternalDefaultScreen { get; }
|
||||||
void InternalChangeScreen(IRoutableScreen? screen);
|
void InternalChangeScreen(IRoutableScreen? screen);
|
||||||
}
|
}
|
||||||
@ -25,7 +25,13 @@ public abstract class RoutableHostScreen<TScreen> : RoutableScreen, IRoutableHos
|
|||||||
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
|
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.InternalScreen => Screen;
|
||||||
|
IRoutableScreen? IRoutableHostScreen.InternalDefaultScreen => DefaultScreen;
|
||||||
|
|
||||||
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
|
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -27,7 +27,13 @@ public abstract class RoutableHostScreen<TScreen, TParam> : RoutableScreen<TPara
|
|||||||
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
|
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.InternalScreen => Screen;
|
||||||
|
IRoutableScreen? IRoutableHostScreen.InternalDefaultScreen => DefaultScreen;
|
||||||
|
|
||||||
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
|
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>
|
/// <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()
|
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>
|
/// <summary>
|
||||||
/// Called while navigating to this screen.
|
/// Called while navigating to this screen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -26,15 +31,16 @@ public abstract class RoutableScreen<TParam> : RoutableScreen, IRoutableScreen w
|
|||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Func<object[], TParam> activator = GetParameterActivator();
|
Func<object[], TParam> activator = GetParameterActivator();
|
||||||
|
|
||||||
if (args.SegmentParameters.Length != _parameterPropertyCount)
|
object[] routeParameters = ParameterSource == ParameterSource.Segment ? args.SegmentParameters : args.RouteParameters;
|
||||||
throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {args.SegmentParameters.Length}.");
|
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(args, cancellationToken);
|
||||||
await OnNavigating(parameters, args, cancellationToken);
|
await OnNavigating(parameters, args, cancellationToken);
|
||||||
}
|
}
|
||||||
@ -97,4 +103,20 @@ public abstract class RoutableScreen<TParam> : RoutableScreen, IRoutableScreen w
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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);
|
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 />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -45,6 +45,12 @@ public interface IRouter
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task containing a boolean value which indicates whether there was a forward path to go back to.</returns>
|
/// <returns>A task containing a boolean value which indicates whether there was a forward path to go back to.</returns>
|
||||||
Task<bool> GoForward();
|
Task<bool> GoForward();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously navigates upwards to the parent route.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<bool> GoUp();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears the navigation history.
|
/// Clears the navigation history.
|
||||||
|
|||||||
@ -109,12 +109,11 @@ internal class Navigation
|
|||||||
// Navigate the child too
|
// Navigate the child too
|
||||||
if (resolution.Child != null)
|
if (resolution.Child != null)
|
||||||
await NavigateResolution(resolution.Child, args, childScreen);
|
await NavigateResolution(resolution.Child, args, childScreen);
|
||||||
// Make sure there is no child
|
// Without a resolution, navigate to the default screen (which may be null)
|
||||||
else if (childScreen.InternalScreen != null)
|
else if (childScreen.InternalScreen != childScreen.InternalDefaultScreen)
|
||||||
childScreen.InternalChangeScreen(null);
|
childScreen.InternalChangeScreen(childScreen.InternalDefaultScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Completed = true;
|
Completed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Reactive.Subjects;
|
using System.Reactive.Subjects;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
@ -72,7 +73,13 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task Navigate(string path, RouterNavigationOptions? options = null)
|
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();
|
options ??= new RouterNavigationOptions();
|
||||||
|
|
||||||
// Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
|
// 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;
|
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 />
|
/// <inheritdoc />
|
||||||
public void ClearHistory()
|
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);
|
_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)
|
private void MainWindowServiceOnMainWindowOpened(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid Margin="20" Grid.Column="0">
|
<Grid Margin="20" Grid.Column="0">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
|
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">TitleTextBlockStyle</TextBlock>
|
||||||
<TextBlock Classes="h1">This is heading 1</TextBlock>
|
<TextBlock Classes="h1">This is heading 1</TextBlock>
|
||||||
<TextBlock Classes="h2">This is heading 2</TextBlock>
|
<TextBlock Classes="h2">This is heading 2</TextBlock>
|
||||||
<TextBlock Classes="h3">This is heading 3</TextBlock>
|
<TextBlock Classes="h3">This is heading 3</TextBlock>
|
||||||
@ -22,6 +23,7 @@
|
|||||||
|
|
||||||
<Grid Margin="20" Grid.Column="1">
|
<Grid Margin="20" Grid.Column="1">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
|
<Border Width="400" Classes="skeleton-text title"></Border>
|
||||||
<Border Width="400" Classes="skeleton-text h1"></Border>
|
<Border Width="400" Classes="skeleton-text h1"></Border>
|
||||||
<Border Width="400" Classes="skeleton-text h2"></Border>
|
<Border Width="400" Classes="skeleton-text h2"></Border>
|
||||||
<Border Width="400" Classes="skeleton-text h3"></Border>
|
<Border Width="400" Classes="skeleton-text h3"></Border>
|
||||||
@ -39,6 +41,7 @@
|
|||||||
<Setter Property="Background" Value="#55ff0000"></Setter>
|
<Setter Property="Background" Value="#55ff0000"></Setter>
|
||||||
</Style>
|
</Style>
|
||||||
</StackPanel.Styles>
|
</StackPanel.Styles>
|
||||||
|
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">TitleTextBlockStyle</TextBlock>
|
||||||
<TextBlock Classes="h1">This is heading 1</TextBlock>
|
<TextBlock Classes="h1">This is heading 1</TextBlock>
|
||||||
<TextBlock Classes="h2">This is heading 2</TextBlock>
|
<TextBlock Classes="h2">This is heading 2</TextBlock>
|
||||||
<TextBlock Classes="h3">This is heading 3</TextBlock>
|
<TextBlock Classes="h3">This is heading 3</TextBlock>
|
||||||
@ -51,6 +54,7 @@
|
|||||||
|
|
||||||
<Grid Margin="20" Grid.Column="0" Row="1">
|
<Grid Margin="20" Grid.Column="0" Row="1">
|
||||||
<StackPanel Spacing="2">
|
<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 h1 no-margin"></Border>
|
||||||
<Border Width="400" Classes="skeleton-text h2 no-margin"></Border>
|
<Border Width="400" Classes="skeleton-text h2 no-margin"></Border>
|
||||||
<Border Width="400" Classes="skeleton-text h3 no-margin"></Border>
|
<Border Width="400" Classes="skeleton-text h3 no-margin"></Border>
|
||||||
@ -68,6 +72,7 @@
|
|||||||
<Setter Property="Background" Value="#55ff0000"></Setter>
|
<Setter Property="Background" Value="#55ff0000"></Setter>
|
||||||
</Style>
|
</Style>
|
||||||
</StackPanel.Styles>
|
</StackPanel.Styles>
|
||||||
|
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">TitleTextBlockStyle</TextBlock>
|
||||||
<TextBlock Classes="h1 no-margin">This is heading 1</TextBlock>
|
<TextBlock Classes="h1 no-margin">This is heading 1</TextBlock>
|
||||||
<TextBlock Classes="h2 no-margin">This is heading 2</TextBlock>
|
<TextBlock Classes="h2 no-margin">This is heading 2</TextBlock>
|
||||||
<TextBlock Classes="h3 no-margin">This is heading 3</TextBlock>
|
<TextBlock Classes="h3 no-margin">This is heading 3</TextBlock>
|
||||||
@ -125,6 +130,11 @@
|
|||||||
</Style.Animations>
|
</Style.Animations>
|
||||||
</Style>
|
</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">
|
<Style Selector="Border.skeleton-text.h1">
|
||||||
<Setter Property="Height" Value="65" />
|
<Setter Property="Height" Value="65" />
|
||||||
<Setter Property="Margin" Value="0 10 0 20" />
|
<Setter Property="Margin" Value="0 10 0 20" />
|
||||||
|
|||||||
@ -44,5 +44,27 @@
|
|||||||
<DependentUpon>DeviceSelectionDialogView.axaml</DependentUpon>
|
<DependentUpon>DeviceSelectionDialogView.axaml</DependentUpon>
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</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>
|
</ItemGroup>
|
||||||
</Project>
|
</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.SurfaceEditor;
|
||||||
using Artemis.UI.Screens.Workshop;
|
using Artemis.UI.Screens.Workshop;
|
||||||
using Artemis.UI.Screens.Workshop.Entries;
|
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.Home;
|
||||||
using Artemis.UI.Screens.Workshop.Layout;
|
using Artemis.UI.Screens.Workshop.Layout;
|
||||||
using Artemis.UI.Screens.Workshop.Library;
|
using Artemis.UI.Screens.Workshop.Library;
|
||||||
using Artemis.UI.Screens.Workshop.Library.Tabs;
|
using Artemis.UI.Screens.Workshop.Library.Tabs;
|
||||||
|
using Artemis.UI.Screens.Workshop.Plugins;
|
||||||
using Artemis.UI.Screens.Workshop.Profile;
|
using Artemis.UI.Screens.Workshop.Profile;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using PluginDetailsViewModel = Artemis.UI.Screens.Workshop.Plugins.PluginDetailsViewModel;
|
using PluginDetailsViewModel = Artemis.UI.Screens.Workshop.Plugins.PluginDetailsViewModel;
|
||||||
|
|
||||||
namespace Artemis.UI.Routing;
|
namespace Artemis.UI.Routing
|
||||||
|
|
||||||
public static class Routes
|
|
||||||
{
|
{
|
||||||
public static List<IRouterRegistration> ArtemisRoutes =
|
public static class Routes
|
||||||
[
|
{
|
||||||
new RouteRegistration<BlankViewModel>("blank"),
|
public static readonly List<IRouterRegistration> ArtemisRoutes =
|
||||||
new RouteRegistration<HomeViewModel>("home"),
|
[
|
||||||
new RouteRegistration<WorkshopViewModel>("workshop")
|
new RouteRegistration<BlankViewModel>("blank"),
|
||||||
{
|
new RouteRegistration<HomeViewModel>("home"),
|
||||||
Children =
|
new RouteRegistration<WorkshopViewModel>("workshop", [
|
||||||
[
|
|
||||||
new RouteRegistration<WorkshopOfflineViewModel>("offline/{message:string}"),
|
new RouteRegistration<WorkshopOfflineViewModel>("offline/{message:string}"),
|
||||||
new RouteRegistration<EntriesViewModel>("entries")
|
new RouteRegistration<EntriesViewModel>("entries", [
|
||||||
{
|
new RouteRegistration<PluginListViewModel>("plugins", [
|
||||||
Children =
|
new RouteRegistration<PluginDetailsViewModel>("details/{entryId:long}", [
|
||||||
[
|
new RouteRegistration<PluginManageViewModel>("manage"),
|
||||||
new RouteRegistration<PluginListViewModel>("plugins")
|
new RouteRegistration<EntryReleaseViewModel>("releases/{releaseId:long}")
|
||||||
{
|
])
|
||||||
Children = [new RouteRegistration<PluginDetailsViewModel>("details/{entryId:long}")]
|
]),
|
||||||
},
|
new RouteRegistration<ProfileListViewModel>("profiles", [
|
||||||
new RouteRegistration<ProfileListViewModel>("profiles")
|
new RouteRegistration<ProfileDetailsViewModel>("details/{entryId:long}", [
|
||||||
{
|
new RouteRegistration<EntryReleaseViewModel>("releases/{releaseId:long}")
|
||||||
Children = [new RouteRegistration<ProfileDetailsViewModel>("details/{entryId:long}")]
|
])
|
||||||
},
|
]),
|
||||||
new RouteRegistration<LayoutListViewModel>("layouts")
|
new RouteRegistration<LayoutListViewModel>("layouts", [
|
||||||
{
|
new RouteRegistration<LayoutDetailsViewModel>("details/{entryId:long}", [
|
||||||
Children = [new RouteRegistration<LayoutDetailsViewModel>("details/{entryId:long}")]
|
new RouteRegistration<LayoutManageViewModel>("manage"),
|
||||||
},
|
new RouteRegistration<EntryReleaseViewModel>("releases/{releaseId:long}")
|
||||||
]
|
])
|
||||||
},
|
])
|
||||||
|
]),
|
||||||
new RouteRegistration<WorkshopLibraryViewModel>("library")
|
new RouteRegistration<WorkshopLibraryViewModel>("library", [
|
||||||
{
|
new RouteRegistration<InstalledTabViewModel>("installed"),
|
||||||
Children =
|
new RouteRegistration<SubmissionsTabViewModel>("submissions"),
|
||||||
[
|
new RouteRegistration<SubmissionManagementViewModel>("submissions/{entryId:long}", [
|
||||||
new RouteRegistration<InstalledTabViewModel>("installed"),
|
new RouteRegistration<SubmissionReleaseViewModel>("releases/{releaseId:long}")
|
||||||
new RouteRegistration<SubmissionsTabViewModel>("submissions"),
|
])
|
||||||
new RouteRegistration<SubmissionDetailViewModel>("submissions/{entryId:long}")
|
])
|
||||||
]
|
]),
|
||||||
}
|
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
|
||||||
]
|
new RouteRegistration<SettingsViewModel>("settings", [
|
||||||
},
|
|
||||||
|
|
||||||
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
|
|
||||||
new RouteRegistration<SettingsViewModel>("settings")
|
|
||||||
{
|
|
||||||
Children =
|
|
||||||
[
|
|
||||||
new RouteRegistration<GeneralTabViewModel>("general"),
|
new RouteRegistration<GeneralTabViewModel>("general"),
|
||||||
new RouteRegistration<PluginsTabViewModel>("plugins"),
|
new RouteRegistration<PluginsTabViewModel>("plugins"),
|
||||||
new RouteRegistration<DevicesTabViewModel>("devices"),
|
new RouteRegistration<DevicesTabViewModel>("devices"),
|
||||||
new RouteRegistration<ReleasesTabViewModel>("releases")
|
new RouteRegistration<ReleasesTabViewModel>("releases", [
|
||||||
{
|
new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")
|
||||||
Children = [new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")]
|
]),
|
||||||
},
|
|
||||||
|
|
||||||
new RouteRegistration<AccountTabViewModel>("account"),
|
new RouteRegistration<AccountTabViewModel>("account"),
|
||||||
new RouteRegistration<AboutTabViewModel>("about")
|
new RouteRegistration<AboutTabViewModel>("about")
|
||||||
]
|
]),
|
||||||
},
|
new RouteRegistration<ProfileEditorViewModel>("profile-editor/{profileConfigurationId:guid}")
|
||||||
|
];
|
||||||
new RouteRegistration<ProfileEditorViewModel>("profile-editor/{profileConfigurationId:guid}")
|
}
|
||||||
];
|
|
||||||
}
|
}
|
||||||
@ -19,7 +19,7 @@ public partial class RootView : ReactiveUserControl<RootViewModel>
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Invoke(() => RootFrame.NavigateFromObject(viewModel));
|
RootFrame.NavigateFromObject(viewModel);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -18,7 +18,7 @@ public partial class SettingsView : ReactiveUserControl<SettingsViewModel>
|
|||||||
|
|
||||||
private void Navigate(ViewModelBase viewModel)
|
private void Navigate(ViewModelBase viewModel)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
|
TabFrame.NavigateFromObject(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
|
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
|
||||||
|
|||||||
@ -17,17 +17,13 @@ public partial class ReleasesTabView : ReactiveUserControl<ReleasesTabViewModel>
|
|||||||
|
|
||||||
private void Navigate(ViewModelBase viewModel)
|
private void Navigate(ViewModelBase viewModel)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Invoke(() =>
|
try
|
||||||
{
|
{
|
||||||
try
|
ReleaseFrame.NavigateFromObject(viewModel);
|
||||||
{
|
}
|
||||||
ReleaseFrame.NavigateFromObject(viewModel);
|
catch (Exception)
|
||||||
}
|
{
|
||||||
catch (Exception)
|
// ignored
|
||||||
{
|
}
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -43,7 +43,7 @@
|
|||||||
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
|
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<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}">
|
<Panel Grid.Column="1" IsVisible="{CompiledBinding InstallationAvailable}">
|
||||||
<!-- Install progress -->
|
<!-- Install progress -->
|
||||||
@ -124,7 +124,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Column="2" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Right">
|
<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="0" Classes="info-title">File size</TextBlock>
|
||||||
<TextBlock Grid.Column="1"
|
<TextBlock Grid.Column="1"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
@ -149,5 +149,4 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -13,71 +13,90 @@
|
|||||||
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
|
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
|
||||||
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<StackPanel>
|
<Panel>
|
||||||
<Panel>
|
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}">
|
||||||
<Border CornerRadius="6"
|
<Border Classes="skeleton-text" Margin="0 0 10 0" Width="80" Height="80"></Border>
|
||||||
HorizontalAlignment="Left"
|
<Border Classes="skeleton-text title" HorizontalAlignment="Stretch"/>
|
||||||
Margin="0 0 10 0"
|
<Border Classes="skeleton-text" Width="120"/>
|
||||||
Width="80"
|
<Border Classes="skeleton-text" Width="140" Margin="0 8"/>
|
||||||
Height="80"
|
<Border Classes="skeleton-text" Width="80"/>
|
||||||
ClipToBounds="True">
|
<Border Classes="card-separator" Margin="0 15 0 17"></Border>
|
||||||
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
|
<Border Classes="skeleton-text" Width="120"/>
|
||||||
</Border>
|
<StackPanel Margin="0 10 0 0">
|
||||||
<Button Classes="icon-button"
|
<Border Classes="skeleton-text" Width="160"/>
|
||||||
VerticalAlignment="Top"
|
<Border Classes="skeleton-text" Width="160"/>
|
||||||
HorizontalAlignment="Right"
|
</StackPanel>
|
||||||
Command="{CompiledBinding CopyShareLink}"
|
<Border Classes="skeleton-button"></Border>
|
||||||
ToolTip.Tip="Copy share link">
|
</StackPanel>
|
||||||
<avalonia:MaterialIcon Kind="ShareVariant" />
|
<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>
|
</Button>
|
||||||
</Panel>
|
</StackPanel>
|
||||||
|
</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>
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -1,10 +1,11 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Entries.Details;
|
namespace Artemis.UI.Screens.Workshop.Entries.Details;
|
||||||
|
|
||||||
public partial class EntryInfoView : UserControl
|
public partial class EntryInfoView : ReactiveUserControl<EntryInfoViewModel>
|
||||||
{
|
{
|
||||||
public EntryInfoView()
|
public EntryInfoView()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,28 +1,66 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using System.Reactive.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.WebClient.Workshop;
|
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;
|
namespace Artemis.UI.Screens.Workshop.Entries.Details;
|
||||||
|
|
||||||
public class EntryInfoViewModel : ViewModelBase
|
public partial class EntryInfoViewModel : ActivatableViewModelBase
|
||||||
{
|
{
|
||||||
|
private readonly IRouter _router;
|
||||||
private readonly INotificationService _notificationService;
|
private readonly INotificationService _notificationService;
|
||||||
public IEntryDetails Entry { get; }
|
private readonly IWorkshopService _workshopService;
|
||||||
public DateTimeOffset? UpdatedAt { get; }
|
[Notify] private IEntryDetails? _entry;
|
||||||
|
[Notify] private DateTimeOffset? _updatedAt;
|
||||||
public EntryInfoViewModel(IEntryDetails entry, INotificationService notificationService)
|
[Notify] private bool _canBeManaged;
|
||||||
|
|
||||||
|
public EntryInfoViewModel(IRouter router, INotificationService notificationService, IWorkshopService workshopService)
|
||||||
{
|
{
|
||||||
|
_router = router;
|
||||||
_notificationService = notificationService;
|
_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;
|
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()
|
public async Task CopyShareLink()
|
||||||
{
|
{
|
||||||
|
if (Entry == null)
|
||||||
|
return;
|
||||||
|
|
||||||
await Shared.UI.Clipboard.SetTextAsync($"{WorkshopConstants.WORKSHOP_URL}/entries/{Entry.Id}/{StringUtilities.UrlFriendly(Entry.Name)}");
|
await Shared.UI.Clipboard.SetTextAsync($"{WorkshopConstants.WORKSHOP_URL}/entries/{Entry.Id}/{StringUtilities.UrlFriendly(Entry.Name)}");
|
||||||
_notificationService.CreateNotification().WithTitle("Copied share link to clipboard.").Show();
|
_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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
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:tagsInput="clr-namespace:Artemis.UI.Shared.TagsInput;assembly=Artemis.UI.Shared"
|
||||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
xmlns:categories="clr-namespace:Artemis.UI.Screens.Workshop.Categories"
|
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: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"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
||||||
x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntrySpecificationsView"
|
x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntrySpecificationsView"
|
||||||
x:DataType="details:EntrySpecificationsViewModel">
|
x:DataType="details:EntrySpecificationsViewModel">
|
||||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
<Grid RowDefinitions="Auto,*,Auto">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<StackPanel.Styles>
|
<StackPanel.Styles>
|
||||||
<Styles>
|
<Styles>
|
||||||
@ -95,48 +92,9 @@
|
|||||||
<tagsInput:TagsInput Tags="{CompiledBinding Tags}" />
|
<tagsInput:TagsInput Tags="{CompiledBinding Tags}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Grid Row="1" ColumnDefinitions="Auto,*">
|
<controls:SplitMarkdownEditor Grid.Row="1" Title="Description" Markdown="{CompiledBinding Description}"/>
|
||||||
<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>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="3"
|
<TextBlock Grid.Row="2"
|
||||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
|
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
|
||||||
Margin="2 8 0 0"
|
Margin="2 8 0 0"
|
||||||
IsVisible="{CompiledBinding !DescriptionValid}">
|
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 Avalonia.ReactiveUI;
|
||||||
using AvaloniaEdit.TextMate;
|
|
||||||
using ReactiveUI;
|
|
||||||
using TextMateSharp.Grammars;
|
|
||||||
using VisualExtensions = Artemis.UI.Shared.Extensions.VisualExtensions;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Entries.Details;
|
namespace Artemis.UI.Screens.Workshop.Entries.Details;
|
||||||
|
|
||||||
public partial class EntrySpecificationsView : ReactiveUserControl<EntrySpecificationsViewModel>
|
public partial class EntrySpecificationsView : ReactiveUserControl<EntrySpecificationsViewModel>
|
||||||
{
|
{
|
||||||
private ScrollViewer? _editorScrollViewer;
|
|
||||||
private ScrollViewer? _previewScrollViewer;
|
|
||||||
private bool _updating;
|
|
||||||
|
|
||||||
public EntrySpecificationsView()
|
public EntrySpecificationsView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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 _summary = string.Empty;
|
||||||
[Notify] private string _description = string.Empty;
|
[Notify] private string _description = string.Empty;
|
||||||
[Notify] private Bitmap? _iconBitmap;
|
[Notify] private Bitmap? _iconBitmap;
|
||||||
[Notify] private TextDocument? _markdownDocument;
|
|
||||||
[Notify(Setter.Private)] private bool _iconChanged;
|
[Notify(Setter.Private)] private bool _iconChanged;
|
||||||
|
|
||||||
public EntrySpecificationsViewModel(IWorkshopClient workshopClient, IWindowService windowService)
|
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);
|
_categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid);
|
||||||
_descriptionValid = descriptionRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.DescriptionValid);
|
_descriptionValid = descriptionRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.DescriptionValid);
|
||||||
|
|
||||||
this.WhenActivatedAsync(async d =>
|
this.WhenActivatedAsync(async _ => await PopulateCategories());
|
||||||
{
|
|
||||||
// Load categories
|
|
||||||
await PopulateCategories();
|
|
||||||
|
|
||||||
MarkdownDocument = new TextDocument(new StringTextSource(Description));
|
|
||||||
MarkdownDocument.TextChanged += MarkdownDocumentOnTextChanged;
|
|
||||||
Disposable.Create(() =>
|
|
||||||
{
|
|
||||||
MarkdownDocument.TextChanged -= MarkdownDocumentOnTextChanged;
|
|
||||||
MarkdownDocument = null;
|
|
||||||
ClearIcon();
|
|
||||||
}).DisposeWith(d);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> SelectIcon { get; }
|
public ReactiveCommand<Unit, Unit> SelectIcon { get; }
|
||||||
@ -92,12 +78,7 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
|
|||||||
public bool DescriptionValid => _descriptionValid.Value;
|
public bool DescriptionValid => _descriptionValid.Value;
|
||||||
|
|
||||||
public List<long> PreselectedCategories { get; set; } = new();
|
public List<long> PreselectedCategories { get; set; } = new();
|
||||||
|
|
||||||
private void MarkdownDocumentOnTextChanged(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
Description = MarkdownDocument?.Text ?? string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExecuteSelectIcon()
|
private async Task ExecuteSelectIcon()
|
||||||
{
|
{
|
||||||
string[]? result = await _windowService.CreateOpenFileDialog()
|
string[]? result = await _windowService.CreateOpenFileDialog()
|
||||||
@ -112,12 +93,6 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
|
|||||||
IconChanged = true;
|
IconChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearIcon()
|
|
||||||
{
|
|
||||||
IconBitmap?.Dispose();
|
|
||||||
IconBitmap = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PopulateCategories()
|
private async Task PopulateCategories()
|
||||||
{
|
{
|
||||||
IOperationResult<IGetCategoriesResult> categories = await _workshopClient.GetCategories.ExecuteAsync();
|
IOperationResult<IGetCategoriesResult> categories = await _workshopClient.GetCategories.ExecuteAsync();
|
||||||
|
|||||||
@ -18,7 +18,7 @@ public partial class EntriesView : ReactiveUserControl<EntriesViewModel>
|
|||||||
|
|
||||||
private void Navigate(ViewModelBase viewModel)
|
private void Navigate(ViewModelBase viewModel)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
|
TabFrame.NavigateFromObject(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
|
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
|
||||||
|
|||||||
@ -53,8 +53,8 @@ public partial class EntriesViewModel : RoutableHostScreen<RoutableScreen>
|
|||||||
|
|
||||||
public void GoBack()
|
public void GoBack()
|
||||||
{
|
{
|
||||||
if (ViewingDetails)
|
if (ViewingDetails && SelectedTab != null)
|
||||||
_router.GoBack();
|
_router.Navigate(SelectedTab.Path);
|
||||||
else
|
else
|
||||||
_router.Navigate("workshop");
|
_router.Navigate("workshop");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,11 +79,11 @@
|
|||||||
<!-- Install state -->
|
<!-- Install state -->
|
||||||
<StackPanel Grid.Column="2" Grid.Row="1" Margin="0 0 4 0" HorizontalAlignment="Right" VerticalAlignment="Bottom" IsVisible="{CompiledBinding IsInstalled}">
|
<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}">
|
<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>
|
<Run>installed</Run>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding UpdateAvailable}">
|
<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>
|
<Run>update available</Run>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</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 System.Threading;
|
||||||
using Artemis.UI.Shared.Routing;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using Avalonia.Threading;
|
|
||||||
using ReactiveUI;
|
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();
|
InitializeComponent();
|
||||||
EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch();
|
EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch();
|
||||||
|
|
||||||
this.WhenActivated(d =>
|
this.WhenActivated(_ => UpdateEntriesPerFetch());
|
||||||
{
|
|
||||||
UpdateEntriesPerFetch();
|
|
||||||
ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
|
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (ViewModel == null)
|
||||||
|
return;
|
||||||
|
|
||||||
// When near the bottom of EntriesScrollViewer, call FetchMore on the view model
|
// 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)
|
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)
|
ViewModel.ScrollOffset = EntriesScrollViewer.Offset;
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateEntriesPerFetch()
|
private void UpdateEntriesPerFetch()
|
||||||
@ -6,6 +6,7 @@ using System.Reactive.Disposables;
|
|||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Extensions;
|
||||||
using Artemis.UI.Screens.Workshop.Categories;
|
using Artemis.UI.Screens.Workshop.Categories;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
@ -15,29 +16,28 @@ using DynamicData;
|
|||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using StrawberryShake;
|
using StrawberryShake;
|
||||||
|
using Vector = Avalonia.Vector;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Entries.List;
|
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 SourceList<IEntrySummary> _entries = new();
|
||||||
private readonly INotificationService _notificationService;
|
private readonly INotificationService _notificationService;
|
||||||
private readonly IWorkshopClient _workshopClient;
|
private readonly IWorkshopClient _workshopClient;
|
||||||
private readonly string _route;
|
|
||||||
private IGetEntriesv2_EntriesV2_PageInfo? _currentPageInfo;
|
private IGetEntriesv2_EntriesV2_PageInfo? _currentPageInfo;
|
||||||
|
|
||||||
[Notify] private bool _initializing = true;
|
[Notify] private bool _initializing = true;
|
||||||
[Notify] private bool _fetchingMore;
|
[Notify] private bool _fetchingMore;
|
||||||
[Notify] private int _entriesPerFetch;
|
[Notify] private int _entriesPerFetch;
|
||||||
|
[Notify] private Vector _scrollOffset;
|
||||||
|
|
||||||
protected EntryListViewModel(string route,
|
protected EntryListViewModel(IWorkshopClient workshopClient,
|
||||||
IWorkshopClient workshopClient,
|
|
||||||
CategoriesViewModel categoriesViewModel,
|
CategoriesViewModel categoriesViewModel,
|
||||||
EntryListInputViewModel entryListInputViewModel,
|
EntryListInputViewModel entryListInputViewModel,
|
||||||
INotificationService notificationService,
|
INotificationService notificationService,
|
||||||
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
|
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
|
||||||
{
|
{
|
||||||
_route = route;
|
|
||||||
_workshopClient = workshopClient;
|
_workshopClient = workshopClient;
|
||||||
_notificationService = notificationService;
|
_notificationService = notificationService;
|
||||||
|
|
||||||
@ -50,37 +50,31 @@ public abstract partial class EntryListViewModel : RoutableHostScreen<RoutableSc
|
|||||||
.Subscribe();
|
.Subscribe();
|
||||||
Entries = entries;
|
Entries = entries;
|
||||||
|
|
||||||
|
// Respond to filter query input changes
|
||||||
this.WhenActivated(d =>
|
this.WhenActivated(d =>
|
||||||
{
|
{
|
||||||
// Respond to filter query input changes
|
|
||||||
InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d);
|
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);
|
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 CategoriesViewModel CategoriesViewModel { get; }
|
||||||
public EntryListInputViewModel InputViewModel { get; }
|
public EntryListInputViewModel InputViewModel { get; }
|
||||||
|
public EntryType? EntryType { get; set; }
|
||||||
|
|
||||||
public ReadOnlyObservableCollection<EntryListItemViewModel> Entries { get; }
|
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)
|
public async Task FetchMore(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (FetchingMore || _currentPageInfo != null && !_currentPageInfo.HasNextPage)
|
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
|
// Sort by created at
|
||||||
if (InputViewModel.SortBy == 1)
|
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()) && 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:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
|
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"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
||||||
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView"
|
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView"
|
||||||
x:DataType="layout:LayoutDetailsViewModel">
|
x:DataType="layout:LayoutDetailsViewModel">
|
||||||
@ -12,21 +13,17 @@
|
|||||||
<Border Classes="card" VerticalAlignment="Top">
|
<Border Classes="card" VerticalAlignment="Top">
|
||||||
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
||||||
</Border>
|
</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}" />
|
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1" Grid.Column="1">
|
<ScrollViewer Grid.Row="1" Grid.Column="1">
|
||||||
<StackPanel Margin="10 0" Spacing="10">
|
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" Margin="10 0">
|
||||||
<Border Classes="card">
|
<controls:Frame.NavigationPageFactory>
|
||||||
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
|
<ui:PageFactory />
|
||||||
<mdxaml:MarkdownScrollViewer.Styles>
|
</controls:Frame.NavigationPageFactory>
|
||||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
</controls:Frame>
|
||||||
</mdxaml:MarkdownScrollViewer.Styles>
|
|
||||||
</mdxaml:MarkdownScrollViewer>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
</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}" Content="{CompiledBinding EntryImagesViewModel}" />
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Layout;
|
namespace Artemis.UI.Screens.Workshop.Layout;
|
||||||
|
|
||||||
@ -7,5 +10,9 @@ public partial class LayoutDetailsView : ReactiveUserControl<LayoutDetailsViewMo
|
|||||||
public LayoutDetailsView()
|
public LayoutDetailsView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.Core;
|
|
||||||
using Artemis.Core.Services;
|
|
||||||
using Artemis.UI.Screens.Workshop.Entries.Details;
|
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.Screens.Workshop.Parameters;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
using Artemis.WebClient.Workshop.Models;
|
|
||||||
using Artemis.WebClient.Workshop.Services;
|
|
||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
using StrawberryShake;
|
using StrawberryShake;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Layout;
|
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 IWorkshopClient _client;
|
||||||
private readonly IDeviceService _deviceService;
|
private readonly LayoutDescriptionViewModel _layoutDescriptionViewModel;
|
||||||
private readonly IWindowService _windowService;
|
|
||||||
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
|
|
||||||
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
|
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
|
||||||
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
|
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
|
||||||
[Notify] private IEntryDetails? _entry;
|
[Notify] private IEntryDetails? _entry;
|
||||||
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
|
|
||||||
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
|
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
|
||||||
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
|
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
|
||||||
|
|
||||||
public LayoutDetailsViewModel(IWorkshopClient client,
|
public LayoutDetailsViewModel(IWorkshopClient client,
|
||||||
IDeviceService deviceService,
|
LayoutDescriptionViewModel layoutDescriptionViewModel,
|
||||||
IWindowService windowService,
|
EntryInfoViewModel entryInfoViewModel,
|
||||||
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
|
|
||||||
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
|
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
|
||||||
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
|
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
_deviceService = deviceService;
|
_layoutDescriptionViewModel = layoutDescriptionViewModel;
|
||||||
_windowService = windowService;
|
|
||||||
_getEntryInfoViewModel = getEntryInfoViewModel;
|
|
||||||
_getEntryReleasesViewModel = getEntryReleasesViewModel;
|
_getEntryReleasesViewModel = getEntryReleasesViewModel;
|
||||||
_getEntryImagesViewModel = getEntryImagesViewModel;
|
_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)
|
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)
|
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
Task grace = Task.Delay(300, cancellationToken);
|
||||||
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
|
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
|
||||||
if (result.IsErrorResult())
|
if (result.IsErrorResult())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Let the UI settle to avoid lag when deep linking
|
||||||
|
await grace;
|
||||||
|
|
||||||
Entry = result.Data?.Entry;
|
Entry = result.Data?.Entry;
|
||||||
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
|
EntryInfoViewModel.SetEntry(Entry);
|
||||||
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
|
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
|
||||||
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
|
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
|
||||||
|
_layoutDescriptionViewModel.Entry = Entry;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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" Classes="h4 no-margin">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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.UI.Screens.Workshop.Image;
|
using Artemis.UI.Screens.Workshop.Image;
|
||||||
using Artemis.UI.Screens.Workshop.Parameters;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Screens.Workshop.SubmissionWizard;
|
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
@ -25,7 +24,7 @@ using EntrySpecificationsViewModel = Artemis.UI.Screens.Workshop.Entries.Details
|
|||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Library;
|
namespace Artemis.UI.Screens.Workshop.Library;
|
||||||
|
|
||||||
public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailParameters>
|
public partial class SubmissionDetailsViewModel : RoutableScreen
|
||||||
{
|
{
|
||||||
private readonly IWorkshopClient _client;
|
private readonly IWorkshopClient _client;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
@ -40,7 +39,7 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
|||||||
[Notify] private EntrySpecificationsViewModel? _entrySpecificationsViewModel;
|
[Notify] private EntrySpecificationsViewModel? _entrySpecificationsViewModel;
|
||||||
[Notify(Setter.Private)] private bool _hasChanges;
|
[Notify(Setter.Private)] private bool _hasChanges;
|
||||||
|
|
||||||
public SubmissionDetailViewModel(IWorkshopClient client,
|
public SubmissionDetailsViewModel(IWorkshopClient client,
|
||||||
IWindowService windowService,
|
IWindowService windowService,
|
||||||
IWorkshopService workshopService,
|
IWorkshopService workshopService,
|
||||||
IRouter router,
|
IRouter router,
|
||||||
@ -56,34 +55,23 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
|||||||
_getExistingImageSubmissionViewModel = getExistingImageSubmissionViewModel;
|
_getExistingImageSubmissionViewModel = getExistingImageSubmissionViewModel;
|
||||||
_getImageSubmissionViewModel = getImageSubmissionViewModel;
|
_getImageSubmissionViewModel = getImageSubmissionViewModel;
|
||||||
|
|
||||||
CreateRelease = ReactiveCommand.CreateFromTask(ExecuteCreateRelease);
|
|
||||||
DeleteSubmission = ReactiveCommand.CreateFromTask(ExecuteDeleteSubmission);
|
|
||||||
ViewWorkshopPage = ReactiveCommand.CreateFromTask(ExecuteViewWorkshopPage);
|
|
||||||
AddImage = ReactiveCommand.CreateFromTask(ExecuteAddImage);
|
AddImage = ReactiveCommand.CreateFromTask(ExecuteAddImage);
|
||||||
DiscardChanges = ReactiveCommand.CreateFromTask(ExecuteDiscardChanges, this.WhenAnyValue(vm => vm.HasChanges));
|
DiscardChanges = ReactiveCommand.CreateFromTask(ExecuteDiscardChanges, this.WhenAnyValue(vm => vm.HasChanges));
|
||||||
SaveChanges = ReactiveCommand.CreateFromTask(ExecuteSaveChanges, this.WhenAnyValue(vm => vm.HasChanges));
|
SaveChanges = ReactiveCommand.CreateFromTask(ExecuteSaveChanges, this.WhenAnyValue(vm => vm.HasChanges));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<ImageSubmissionViewModel> Images { get; } = new();
|
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> AddImage { get; }
|
||||||
public ReactiveCommand<Unit, Unit> SaveChanges { get; }
|
public ReactiveCommand<Unit, Unit> SaveChanges { get; }
|
||||||
public ReactiveCommand<Unit, Unit> DiscardChanges { 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);
|
Entry = entry;
|
||||||
if (result.IsErrorResult())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Entry = result.Data?.Entry;
|
|
||||||
await ApplyDetailsFromEntry(cancellationToken);
|
await ApplyDetailsFromEntry(cancellationToken);
|
||||||
ApplyImagesFromEntry();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnClosing(NavigationArguments args)
|
public async Task OnClosing(NavigationArguments args)
|
||||||
{
|
{
|
||||||
if (!HasChanges)
|
if (!HasChanges)
|
||||||
return;
|
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?");
|
bool confirmed = await _windowService.ShowConfirmContentDialog("You have unsaved changes", "Do you want to discard your unsaved changes?");
|
||||||
if (!confirmed)
|
if (!confirmed)
|
||||||
args.Cancel();
|
args.Cancel();
|
||||||
|
else
|
||||||
|
await ExecuteDiscardChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplyDetailsFromEntry(CancellationToken cancellationToken)
|
private async Task ApplyDetailsFromEntry(CancellationToken cancellationToken)
|
||||||
@ -106,6 +96,7 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
|||||||
if (Entry == null)
|
if (Entry == null)
|
||||||
{
|
{
|
||||||
EntrySpecificationsViewModel = null;
|
EntrySpecificationsViewModel = null;
|
||||||
|
ApplyImagesFromEntry();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +179,6 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
|||||||
private async Task ExecuteDiscardChanges()
|
private async Task ExecuteDiscardChanges()
|
||||||
{
|
{
|
||||||
await ApplyDetailsFromEntry(CancellationToken.None);
|
await ApplyDetailsFromEntry(CancellationToken.None);
|
||||||
ApplyImagesFromEntry();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ExecuteSaveChanges(CancellationToken cancellationToken)
|
private async Task ExecuteSaveChanges(CancellationToken cancellationToken)
|
||||||
@ -243,30 +233,7 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
|||||||
HasChanges = false;
|
HasChanges = false;
|
||||||
await _router.Reload();
|
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)
|
private async Task ExecuteAddImage(CancellationToken arg)
|
||||||
{
|
{
|
||||||
string[]? result = await _windowService.CreateOpenFileDialog().WithAllowMultiple().HavingFilter(f => f.WithBitmaps()).ShowAsync();
|
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)
|
private void InputChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
UpdateHasChanges();
|
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)
|
private void Navigate(ViewModelBase viewModel)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
|
TabFrame.NavigateFromObject(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
|
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
|
||||||
{
|
{
|
||||||
ViewModel?.GoBack();
|
ViewModel?.GoBack();
|
||||||
|
|||||||
@ -53,7 +53,7 @@ public partial class WorkshopLibraryViewModel : RoutableHostScreen<RoutableScree
|
|||||||
public void GoBack()
|
public void GoBack()
|
||||||
{
|
{
|
||||||
if (ViewingDetails)
|
if (ViewingDetails)
|
||||||
_router.GoBack();
|
_router.Navigate("workshop/library/submissions");
|
||||||
else
|
else
|
||||||
_router.Navigate("workshop");
|
_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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
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:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
|
||||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
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"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginDetailsView"
|
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginDetailsView"
|
||||||
x:DataType="plugins:PluginDetailsViewModel">
|
x:DataType="plugins:PluginDetailsViewModel">
|
||||||
@ -15,47 +16,44 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.PluginInfo, Converter={x:Static ObjectConverters.IsNotNull}}">
|
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.PluginInfo, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
<StackPanel>
|
<Panel>
|
||||||
<TextBlock>Admin required</TextBlock>
|
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}">
|
||||||
<TextBlock Text="Yes" IsVisible="{CompiledBinding Entry.PluginInfo.RequiresAdmin}" />
|
<Border Width="110" Classes="skeleton-text"></Border>
|
||||||
<TextBlock Text="No" IsVisible="{CompiledBinding !Entry.PluginInfo.RequiresAdmin}" />
|
<Border Width="35" Classes="skeleton-text"></Border>
|
||||||
|
|
||||||
<TextBlock Margin="0 15 0 5">Supported platforms</TextBlock>
|
<Border Margin="0 16 0 3" Width="130" Classes="skeleton-text"></Border>
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
<Border Width="60" Classes="skeleton-text"></Border>
|
||||||
<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>
|
||||||
</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>
|
||||||
|
|
||||||
<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}" />
|
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1" Grid.Column="1">
|
<ScrollViewer Grid.Row="1" Grid.Column="1">
|
||||||
<StackPanel Margin="10 0" Spacing="10">
|
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" Margin="10 0">
|
||||||
<Border Classes="card">
|
<controls:Frame.NavigationPageFactory>
|
||||||
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
|
<ui:PageFactory />
|
||||||
<mdxaml:MarkdownScrollViewer.Styles>
|
</controls:Frame.NavigationPageFactory>
|
||||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
</controls:Frame>
|
||||||
</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>
|
|
||||||
</ScrollViewer>
|
</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>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -1,4 +1,7 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Plugins;
|
namespace Artemis.UI.Screens.Workshop.Plugins;
|
||||||
|
|
||||||
@ -7,5 +10,9 @@ public partial class PluginDetailsView : ReactiveUserControl<PluginDetailsViewMo
|
|||||||
public PluginDetailsView()
|
public PluginDetailsView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.Core;
|
|
||||||
using Artemis.Core.Services;
|
|
||||||
using Artemis.UI.Screens.Workshop.Entries.Details;
|
using Artemis.UI.Screens.Workshop.Entries.Details;
|
||||||
using Artemis.UI.Screens.Workshop.Entries.List;
|
using Artemis.UI.Screens.Workshop.Entries.List;
|
||||||
|
using Artemis.UI.Screens.Workshop.EntryReleases;
|
||||||
using Artemis.UI.Screens.Workshop.Parameters;
|
using Artemis.UI.Screens.Workshop.Parameters;
|
||||||
using Artemis.UI.Screens.Workshop.Plugins.Dialogs;
|
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
using Artemis.WebClient.Workshop.Models;
|
|
||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
using StrawberryShake;
|
using StrawberryShake;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Plugins;
|
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 IWorkshopClient _client;
|
||||||
private readonly IWindowService _windowService;
|
private readonly PluginDescriptionViewModel _pluginDescriptionViewModel;
|
||||||
private readonly IPluginManagementService _pluginManagementService;
|
|
||||||
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
|
|
||||||
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
|
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
|
||||||
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
|
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
|
||||||
private readonly Func<IEntrySummary, EntryListItemViewModel> _getEntryListViewModel;
|
|
||||||
[Notify] private IGetPluginEntryById_Entry? _entry;
|
[Notify] private IGetPluginEntryById_Entry? _entry;
|
||||||
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
|
|
||||||
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
|
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
|
||||||
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
|
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
|
||||||
[Notify] private ReadOnlyObservableCollection<EntryListItemViewModel>? _dependants;
|
[Notify] private ReadOnlyObservableCollection<EntryListItemViewModel>? _dependants;
|
||||||
|
|
||||||
public PluginDetailsViewModel(IWorkshopClient client,
|
public PluginDetailsViewModel(IWorkshopClient client,
|
||||||
IWindowService windowService,
|
PluginDescriptionViewModel pluginDescriptionViewModel,
|
||||||
IPluginManagementService pluginManagementService,
|
EntryInfoViewModel entryInfoViewModel,
|
||||||
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
|
|
||||||
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
|
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
|
||||||
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel,
|
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
|
||||||
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
|
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
_windowService = windowService;
|
_pluginDescriptionViewModel = pluginDescriptionViewModel;
|
||||||
_pluginManagementService = pluginManagementService;
|
|
||||||
_getEntryInfoViewModel = getEntryInfoViewModel;
|
|
||||||
_getEntryReleasesViewModel = getEntryReleasesViewModel;
|
_getEntryReleasesViewModel = getEntryReleasesViewModel;
|
||||||
_getEntryImagesViewModel = getEntryImagesViewModel;
|
_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)
|
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)
|
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
Task grace = Task.Delay(300, cancellationToken);
|
||||||
IOperationResult<IGetPluginEntryByIdResult> result = await _client.GetPluginEntryById.ExecuteAsync(entryId, cancellationToken);
|
IOperationResult<IGetPluginEntryByIdResult> result = await _client.GetPluginEntryById.ExecuteAsync(entryId, cancellationToken);
|
||||||
if (result.IsErrorResult())
|
if (result.IsErrorResult())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Let the UI settle to avoid lag when deep linking
|
||||||
|
await grace;
|
||||||
|
|
||||||
Entry = result.Data?.Entry;
|
Entry = result.Data?.Entry;
|
||||||
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
|
EntryInfoViewModel.SetEntry(Entry);
|
||||||
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
|
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
|
||||||
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
|
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
|
||||||
|
|
||||||
if (EntryReleasesViewModel != null)
|
await _pluginDescriptionViewModel.SetEntry(Entry, cancellationToken);
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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" Classes="h4 no-margin">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:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
|
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"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
||||||
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView"
|
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView"
|
||||||
x:DataType="profile:ProfileDetailsViewModel">
|
x:DataType="profile:ProfileDetailsViewModel">
|
||||||
@ -12,34 +13,19 @@
|
|||||||
<Border Classes="card" VerticalAlignment="Top">
|
<Border Classes="card" VerticalAlignment="Top">
|
||||||
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
||||||
</Border>
|
</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}" />
|
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1" Grid.Column="1">
|
<ScrollViewer Grid.Row="1" Grid.Column="1">
|
||||||
<StackPanel Margin="10 0" Spacing="10">
|
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" Margin="10 0">
|
||||||
<Border Classes="card">
|
<controls:Frame.NavigationPageFactory>
|
||||||
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
|
<ui:PageFactory />
|
||||||
<mdxaml:MarkdownScrollViewer.Styles>
|
</controls:Frame.NavigationPageFactory>
|
||||||
<StyleInclude Source="/Styles/Markdown.axaml" />
|
</controls:Frame>
|
||||||
</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>
|
|
||||||
</ScrollViewer>
|
</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>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -1,4 +1,7 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Profile;
|
namespace Artemis.UI.Screens.Workshop.Profile;
|
||||||
|
|
||||||
@ -7,5 +10,9 @@ public partial class ProfileDetailsView : ReactiveUserControl<ProfileDetailsView
|
|||||||
public ProfileDetailsView()
|
public ProfileDetailsView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
|
||||||
|
.WhereNotNull()
|
||||||
|
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
|
||||||
|
.DisposeWith(d));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,7 +5,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.UI.Screens.Workshop.Entries.Details;
|
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.Parameters;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
@ -14,52 +14,56 @@ using StrawberryShake;
|
|||||||
|
|
||||||
namespace Artemis.UI.Screens.Workshop.Profile;
|
namespace Artemis.UI.Screens.Workshop.Profile;
|
||||||
|
|
||||||
public partial class ProfileDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
|
public partial class ProfileDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters>
|
||||||
{
|
{
|
||||||
private readonly IWorkshopClient _client;
|
private readonly IWorkshopClient _client;
|
||||||
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
|
private readonly ProfileDescriptionViewModel _profileDescriptionViewModel;
|
||||||
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
|
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
|
||||||
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
|
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
|
||||||
private readonly Func<IEntrySummary, EntryListItemViewModel> _getEntryListViewModel;
|
|
||||||
|
|
||||||
[Notify] private IEntryDetails? _entry;
|
[Notify] private IEntryDetails? _entry;
|
||||||
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
|
|
||||||
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
|
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
|
||||||
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
|
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
|
||||||
[Notify] private ReadOnlyObservableCollection<EntryListItemViewModel>? _dependencies;
|
|
||||||
|
|
||||||
public ProfileDetailsViewModel(IWorkshopClient client,
|
public ProfileDetailsViewModel(IWorkshopClient client,
|
||||||
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
|
ProfileDescriptionViewModel profileDescriptionViewModel,
|
||||||
|
EntryInfoViewModel entryInfoViewModel,
|
||||||
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
|
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
|
||||||
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel,
|
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
|
||||||
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
|
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
_getEntryInfoViewModel = getEntryInfoViewModel;
|
_profileDescriptionViewModel = profileDescriptionViewModel;
|
||||||
_getEntryReleasesViewModel = getEntryReleasesViewModel;
|
_getEntryReleasesViewModel = getEntryReleasesViewModel;
|
||||||
_getEntryImagesViewModel = getEntryImagesViewModel;
|
_getEntryImagesViewModel = getEntryImagesViewModel;
|
||||||
_getEntryListViewModel = getEntryListViewModel;
|
|
||||||
|
EntryInfoViewModel = entryInfoViewModel;
|
||||||
|
RecycleScreen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override RoutableScreen DefaultScreen => _profileDescriptionViewModel;
|
||||||
|
public EntryInfoViewModel EntryInfoViewModel { get; }
|
||||||
|
|
||||||
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
|
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)
|
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
Task grace = Task.Delay(300, cancellationToken);
|
||||||
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
|
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
|
||||||
if (result.IsErrorResult())
|
if (result.IsErrorResult())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Let the UI settle to avoid lag when deep linking
|
||||||
|
await grace;
|
||||||
|
|
||||||
Entry = result.Data?.Entry;
|
Entry = result.Data?.Entry;
|
||||||
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
|
EntryInfoViewModel.SetEntry(Entry);
|
||||||
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
|
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
|
||||||
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
|
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
|
||||||
|
|
||||||
IReadOnlyList<IEntrySummary>? dependencies = (await _client.GetLatestDependencies.ExecuteAsync(entryId, cancellationToken)).Data?.Entry?.LatestRelease?.Dependencies;
|
await _profileDescriptionViewModel.SetEntry(Entry, cancellationToken);
|
||||||
Dependencies = dependencies != null && dependencies.Any()
|
|
||||||
? new ReadOnlyObservableCollection<EntryListItemViewModel>(new ObservableCollection<EntryListItemViewModel>(dependencies.Select(_getEntryListViewModel)))
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileListView"
|
||||||
|
x:DataType="profile:ProfileListViewModel">
|
||||||
|
<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.Profile;
|
||||||
|
|
||||||
|
public partial class ProfileListView : ReactiveUserControl<ProfileListViewModel>
|
||||||
|
{
|
||||||
|
public ProfileListView()
|
||||||
|
{
|
||||||
|
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.Profile;
|
||||||
|
|
||||||
|
public class ProfileListViewModel : RoutableHostScreen<RoutableScreen>
|
||||||
|
{
|
||||||
|
private readonly EntryListViewModel _entryListViewModel;
|
||||||
|
public override RoutableScreen DefaultScreen => _entryListViewModel;
|
||||||
|
|
||||||
|
public ProfileListViewModel(EntryListViewModel entryListViewModel)
|
||||||
|
{
|
||||||
|
_entryListViewModel = entryListViewModel;
|
||||||
|
_entryListViewModel.EntryType = EntryType.Profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -38,6 +38,7 @@ public class SubmissionWizardState : IDisposable
|
|||||||
public List<ImageUploadRequest> Images { get; set; } = new();
|
public List<ImageUploadRequest> Images { get; set; } = new();
|
||||||
|
|
||||||
public IEntrySource? EntrySource { get; set; }
|
public IEntrySource? EntrySource { get; set; }
|
||||||
|
public string? Changelog { get; set; }
|
||||||
|
|
||||||
public void ChangeScreen<TSubmissionViewModel>() where TSubmissionViewModel : SubmissionViewModel
|
public void ChangeScreen<TSubmissionViewModel>() where TSubmissionViewModel : SubmissionViewModel
|
||||||
{
|
{
|
||||||
|
|||||||
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