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

Merge branch 'development'

This commit is contained in:
Robert 2024-04-14 16:00:08 +02:00
commit 058513fd2a
133 changed files with 2683 additions and 1415 deletions

View File

@ -7,6 +7,7 @@ public class LayoutSelection : CorePropertyChanged
{
private string? _type;
private string? _parameter;
private string? _errorState;
/// <summary>
/// Gets or sets what kind of layout reference this is.
@ -25,4 +26,13 @@ public class LayoutSelection : CorePropertyChanged
get => _parameter;
set => SetAndNotify(ref _parameter, value);
}
/// <summary>
/// Gets or sets the error state of the layout reference.
/// </summary>
public string? ErrorState
{
get => _errorState;
set => SetAndNotify(ref _errorState, value);
}
}

View File

@ -184,13 +184,14 @@ internal class DeviceService : IDeviceService
device.ApplyLayout(null, false, false);
else
provider?.ApplyLayout(device, layout);
UpdateLeds();
}
catch (Exception e)
{
device.LayoutSelection.ErrorState = e.Message;
_logger.Error(e, "Failed to apply device layout");
}
UpdateLeds();
}
/// <inheritdoc />

View File

@ -9,5 +9,6 @@ internal interface IRoutableHostScreen : IRoutableScreen
{
bool RecycleScreen { get; }
IRoutableScreen? InternalScreen { get; }
IRoutableScreen? InternalDefaultScreen { get; }
void InternalChangeScreen(IRoutableScreen? screen);
}

View File

@ -25,7 +25,13 @@ public abstract class RoutableHostScreen<TScreen> : RoutableScreen, IRoutableHos
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
}
/// <summary>
/// Gets the screen to show when no other screen is active.
/// </summary>
public virtual TScreen? DefaultScreen { get; }
IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen;
IRoutableScreen? IRoutableHostScreen.InternalDefaultScreen => DefaultScreen;
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
{

View File

@ -27,7 +27,13 @@ public abstract class RoutableHostScreen<TScreen, TParam> : RoutableScreen<TPara
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
}
/// <summary>
/// Gets the screen to show when no other screen is active.
/// </summary>
public virtual TScreen? DefaultScreen { get; }
IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen;
IRoutableScreen? IRoutableHostScreen.InternalDefaultScreen => DefaultScreen;
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
{

View File

@ -13,6 +13,11 @@ namespace Artemis.UI.Shared.Routing;
/// <typeparam name="TParam">The type of parameters the screen expects. It must have a parameterless constructor.</typeparam>
public abstract class RoutableScreen<TParam> : RoutableScreen, IRoutableScreen where TParam : new()
{
/// <summary>
/// Gets or sets the parameter source of the screen.
/// </summary>
protected ParameterSource ParameterSource { get; set; } = ParameterSource.Segment;
/// <summary>
/// Called while navigating to this screen.
/// </summary>
@ -26,15 +31,16 @@ public abstract class RoutableScreen<TParam> : RoutableScreen, IRoutableScreen w
{
return Task.CompletedTask;
}
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
Func<object[], TParam> activator = GetParameterActivator();
if (args.SegmentParameters.Length != _parameterPropertyCount)
throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {args.SegmentParameters.Length}.");
object[] routeParameters = ParameterSource == ParameterSource.Segment ? args.SegmentParameters : args.RouteParameters;
if (routeParameters.Length != _parameterPropertyCount)
throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {routeParameters.Length}.");
TParam parameters = activator(args.SegmentParameters);
TParam parameters = activator(routeParameters);
await OnNavigating(args, cancellationToken);
await OnNavigating(parameters, args, cancellationToken);
}
@ -97,4 +103,20 @@ public abstract class RoutableScreen<TParam> : RoutableScreen, IRoutableScreen w
}
#endregion
}
/// <summary>
/// Enum representing the source of parameters in the RoutableScreen class.
/// </summary>
public enum ParameterSource
{
/// <summary>
/// Represents the source where parameters are obtained from the segment of the route.
/// </summary>
Segment,
/// <summary>
/// Represents the source where parameters are obtained from the entire route.
/// </summary>
Route
}

View File

@ -18,6 +18,17 @@ public class RouteRegistration<TViewModel> : IRouterRegistration where TViewMode
Route = new Route(path);
}
/// <summary>
/// Initializes a new instance of the <see cref="RouteRegistration{TViewModel}" /> class.
/// </summary>
/// <param name="path">The path of the route.</param>
/// <param name="children">The children of the route.</param>
public RouteRegistration(string path, List<IRouterRegistration> children)
{
Route = new Route(path);
Children = children;
}
/// <inheritdoc />
public override string ToString()
{

View File

@ -45,6 +45,12 @@ public interface IRouter
/// </summary>
/// <returns>A task containing a boolean value which indicates whether there was a forward path to go back to.</returns>
Task<bool> GoForward();
/// <summary>
/// Asynchronously navigates upwards to the parent route.
/// </summary>
/// <returns></returns>
Task<bool> GoUp();
/// <summary>
/// Clears the navigation history.

View File

@ -109,12 +109,11 @@ internal class Navigation
// Navigate the child too
if (resolution.Child != null)
await NavigateResolution(resolution.Child, args, childScreen);
// Make sure there is no child
else if (childScreen.InternalScreen != null)
childScreen.InternalChangeScreen(null);
// Without a resolution, navigate to the default screen (which may be null)
else if (childScreen.InternalScreen != childScreen.InternalDefaultScreen)
childScreen.InternalChangeScreen(childScreen.InternalDefaultScreen);
}
Completed = true;
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using Artemis.Core;
@ -72,7 +73,13 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
/// <inheritdoc />
public async Task Navigate(string path, RouterNavigationOptions? options = null)
{
path = path.ToLower().Trim(' ', '/', '\\');
if (path.StartsWith('/') && _currentRouteSubject.Value != null)
path = _currentRouteSubject.Value + path;
if (path.StartsWith("../") && _currentRouteSubject.Value != null)
path = NavigateUp(_currentRouteSubject.Value, path);
else
path = path.ToLower().Trim(' ', '/', '\\');
options ??= new RouterNavigationOptions();
// Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
@ -161,6 +168,28 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
return true;
}
/// <inheritdoc />
public async Task<bool> GoUp()
{
string? currentPath = _currentRouteSubject.Value;
// Keep removing segments until we find a parent route that resolves
while (currentPath != null && currentPath.Contains('/'))
{
string parentPath = currentPath[..currentPath.LastIndexOf('/')];
RouteResolution resolution = Resolve(parentPath);
if (resolution.Success)
{
await Navigate(parentPath, new RouterNavigationOptions {AddToHistory = false});
return true;
}
currentPath = parentPath;
}
return false;
}
/// <inheritdoc />
public void ClearHistory()
{
@ -194,6 +223,24 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
_logger.Debug("Router disposed, should that be? Stacktrace: \r\n{StackTrace}", Environment.StackTrace);
}
private string NavigateUp(string current, string path)
{
string[] pathParts = current.Split('/');
string[] navigateParts = path.Split('/');
int upCount = navigateParts.TakeWhile(part => part == "..").Count();
if (upCount >= pathParts.Length)
{
throw new InvalidOperationException("Cannot navigate up beyond the root");
}
IEnumerable<string> remainingCurrentPathParts = pathParts.Take(pathParts.Length - upCount);
IEnumerable<string> remainingNavigatePathParts = navigateParts.Skip(upCount);
return string.Join("/", remainingCurrentPathParts.Concat(remainingNavigatePathParts));
}
private void MainWindowServiceOnMainWindowOpened(object? sender, EventArgs e)
{

View File

@ -8,6 +8,7 @@
</Grid.RowDefinitions>
<Grid Margin="20" Grid.Column="0">
<StackPanel>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">TitleTextBlockStyle</TextBlock>
<TextBlock Classes="h1">This is heading 1</TextBlock>
<TextBlock Classes="h2">This is heading 2</TextBlock>
<TextBlock Classes="h3">This is heading 3</TextBlock>
@ -22,6 +23,7 @@
<Grid Margin="20" Grid.Column="1">
<StackPanel>
<Border Width="400" Classes="skeleton-text title"></Border>
<Border Width="400" Classes="skeleton-text h1"></Border>
<Border Width="400" Classes="skeleton-text h2"></Border>
<Border Width="400" Classes="skeleton-text h3"></Border>
@ -39,6 +41,7 @@
<Setter Property="Background" Value="#55ff0000"></Setter>
</Style>
</StackPanel.Styles>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">TitleTextBlockStyle</TextBlock>
<TextBlock Classes="h1">This is heading 1</TextBlock>
<TextBlock Classes="h2">This is heading 2</TextBlock>
<TextBlock Classes="h3">This is heading 3</TextBlock>
@ -51,6 +54,7 @@
<Grid Margin="20" Grid.Column="0" Row="1">
<StackPanel Spacing="2">
<Border Width="400" Classes="skeleton-text title no-margin"></Border>
<Border Width="400" Classes="skeleton-text h1 no-margin"></Border>
<Border Width="400" Classes="skeleton-text h2 no-margin"></Border>
<Border Width="400" Classes="skeleton-text h3 no-margin"></Border>
@ -68,6 +72,7 @@
<Setter Property="Background" Value="#55ff0000"></Setter>
</Style>
</StackPanel.Styles>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">TitleTextBlockStyle</TextBlock>
<TextBlock Classes="h1 no-margin">This is heading 1</TextBlock>
<TextBlock Classes="h2 no-margin">This is heading 2</TextBlock>
<TextBlock Classes="h3 no-margin">This is heading 3</TextBlock>
@ -125,6 +130,11 @@
</Style.Animations>
</Style>
<Style Selector="Border.skeleton-text.title">
<Setter Property="Height" Value="28" />
<Setter Property="Margin" Value="0 5 0 5" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Border.skeleton-text.h1">
<Setter Property="Height" Value="65" />
<Setter Property="Margin" Value="0 10 0 20" />

View File

@ -44,5 +44,27 @@
<DependentUpon>DeviceSelectionDialogView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\Layout\LayoutListView.axaml.cs">
<DependentUpon>LayoutListView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\Plugins\PluginListView.axaml.cs">
<DependentUpon>LayoutListView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\Profile\ProfileListView.axaml.cs">
<DependentUpon>LayoutListView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\EntryReleases\EntryReleasesView.axaml.cs">
<DependentUpon>EntryReleasesView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Screens\Workshop\Entries\Tabs\PluginListView.axaml" />
<UpToDateCheckInput Remove="Screens\Workshop\Entries\Tabs\ProfileListView.axaml" />
<UpToDateCheckInput Remove="Screens\Workshop\Plugins\Dialogs\PluginDialogView.axaml" />
</ItemGroup>
</Project>

View 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&amp;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>

View 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;
}
}

View File

@ -7,77 +7,65 @@ using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.Workshop;
using Artemis.UI.Screens.Workshop.Entries;
using Artemis.UI.Screens.Workshop.Entries.Tabs;
using Artemis.UI.Screens.Workshop.EntryReleases;
using Artemis.UI.Screens.Workshop.Home;
using Artemis.UI.Screens.Workshop.Layout;
using Artemis.UI.Screens.Workshop.Library;
using Artemis.UI.Screens.Workshop.Library.Tabs;
using Artemis.UI.Screens.Workshop.Plugins;
using Artemis.UI.Screens.Workshop.Profile;
using Artemis.UI.Shared.Routing;
using PluginDetailsViewModel = Artemis.UI.Screens.Workshop.Plugins.PluginDetailsViewModel;
namespace Artemis.UI.Routing;
public static class Routes
namespace Artemis.UI.Routing
{
public static List<IRouterRegistration> ArtemisRoutes =
[
new RouteRegistration<BlankViewModel>("blank"),
new RouteRegistration<HomeViewModel>("home"),
new RouteRegistration<WorkshopViewModel>("workshop")
{
Children =
[
public static class Routes
{
public static readonly List<IRouterRegistration> ArtemisRoutes =
[
new RouteRegistration<BlankViewModel>("blank"),
new RouteRegistration<HomeViewModel>("home"),
new RouteRegistration<WorkshopViewModel>("workshop", [
new RouteRegistration<WorkshopOfflineViewModel>("offline/{message:string}"),
new RouteRegistration<EntriesViewModel>("entries")
{
Children =
[
new RouteRegistration<PluginListViewModel>("plugins")
{
Children = [new RouteRegistration<PluginDetailsViewModel>("details/{entryId:long}")]
},
new RouteRegistration<ProfileListViewModel>("profiles")
{
Children = [new RouteRegistration<ProfileDetailsViewModel>("details/{entryId:long}")]
},
new RouteRegistration<LayoutListViewModel>("layouts")
{
Children = [new RouteRegistration<LayoutDetailsViewModel>("details/{entryId:long}")]
},
]
},
new RouteRegistration<WorkshopLibraryViewModel>("library")
{
Children =
[
new RouteRegistration<InstalledTabViewModel>("installed"),
new RouteRegistration<SubmissionsTabViewModel>("submissions"),
new RouteRegistration<SubmissionDetailViewModel>("submissions/{entryId:long}")
]
}
]
},
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
new RouteRegistration<SettingsViewModel>("settings")
{
Children =
[
new RouteRegistration<EntriesViewModel>("entries", [
new RouteRegistration<PluginListViewModel>("plugins", [
new RouteRegistration<PluginDetailsViewModel>("details/{entryId:long}", [
new RouteRegistration<PluginManageViewModel>("manage"),
new RouteRegistration<EntryReleaseViewModel>("releases/{releaseId:long}")
])
]),
new RouteRegistration<ProfileListViewModel>("profiles", [
new RouteRegistration<ProfileDetailsViewModel>("details/{entryId:long}", [
new RouteRegistration<EntryReleaseViewModel>("releases/{releaseId:long}")
])
]),
new RouteRegistration<LayoutListViewModel>("layouts", [
new RouteRegistration<LayoutDetailsViewModel>("details/{entryId:long}", [
new RouteRegistration<LayoutManageViewModel>("manage"),
new RouteRegistration<EntryReleaseViewModel>("releases/{releaseId:long}")
])
])
]),
new RouteRegistration<WorkshopLibraryViewModel>("library", [
new RouteRegistration<InstalledTabViewModel>("installed"),
new RouteRegistration<SubmissionsTabViewModel>("submissions"),
new RouteRegistration<SubmissionManagementViewModel>("submissions/{entryId:long}", [
new RouteRegistration<SubmissionReleaseViewModel>("releases/{releaseId:long}")
])
])
]),
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
new RouteRegistration<SettingsViewModel>("settings", [
new RouteRegistration<GeneralTabViewModel>("general"),
new RouteRegistration<PluginsTabViewModel>("plugins"),
new RouteRegistration<DevicesTabViewModel>("devices"),
new RouteRegistration<ReleasesTabViewModel>("releases")
{
Children = [new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")]
},
new RouteRegistration<ReleasesTabViewModel>("releases", [
new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")
]),
new RouteRegistration<AccountTabViewModel>("account"),
new RouteRegistration<AboutTabViewModel>("about")
]
},
new RouteRegistration<ProfileEditorViewModel>("profile-editor/{profileConfigurationId:guid}")
];
]),
new RouteRegistration<ProfileEditorViewModel>("profile-editor/{profileConfigurationId:guid}")
];
}
}

View File

@ -9,100 +9,111 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Device.Layout.DeviceLayoutTabView"
x:DataType="layout:DeviceLayoutTabViewModel">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<Border Classes="card" Margin="5">
<Grid RowDefinitions="*,Auto">
<StackPanel Grid.Row="0">
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="0" Grid.Column="0">
<TextBlock Text="Default layout file path" />
<TextBlock Classes="subtitle" FontSize="12" TextWrapping="Wrap" Text="{CompiledBinding DefaultLayoutPath}" />
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<Button
Classes="icon-button"
HorizontalAlignment="Right"
IsEnabled="{CompiledBinding !!DefaultLayoutPath}"
ToolTip.Tip="Copy layout file path to clipboard"
Click="LayoutPathButton_OnClick">
<avalonia:MaterialIcon Kind="ContentCopy" Width="18" Height="18" />
</Button>
</StackPanel>
</Grid>
<Grid RowDefinitions="Auto,*">
<controls:InfoBar Grid.Row="0"
Title="Failed to apply layout"
IsOpen="{CompiledBinding Device.LayoutSelection.ErrorState, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Message="{CompiledBinding Device.LayoutSelection.ErrorState}"
Severity="Error"
IsClosable="False"
Margin="5 0"/>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Image file path" />
<TextBlock Classes="subtitle" FontSize="12" TextWrapping="Wrap" Text="{CompiledBinding ImagePath, TargetNullValue=None}" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
<Button
Classes="icon-button"
HorizontalAlignment="Right"
IsEnabled="{CompiledBinding !!ImagePath}"
ToolTip.Tip="Copy image file path to clipboard"
Click="ImagePathButton_OnClick">
<avalonia:MaterialIcon Kind="ContentCopy" Width="18" Height="18" />
</Button>
</StackPanel>
</Grid>
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<Border Classes="card" Margin="5">
<Grid RowDefinitions="*,Auto">
<StackPanel Grid.Row="0">
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="0" Grid.Column="0">
<TextBlock Text="Default layout file path" />
<TextBlock Classes="subtitle" FontSize="12" TextWrapping="Wrap" Text="{CompiledBinding DefaultLayoutPath}" />
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<Button
Classes="icon-button"
HorizontalAlignment="Right"
IsEnabled="{CompiledBinding !!DefaultLayoutPath}"
ToolTip.Tip="Copy layout file path to clipboard"
Click="LayoutPathButton_OnClick">
<avalonia:MaterialIcon Kind="ContentCopy" Width="18" Height="18" />
</Button>
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Layout provider" />
<TextBlock Classes="subtitle" FontSize="12" Text="Choose between different ways to load a layout for this device." />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
<StackPanel.Styles>
<Style Selector="ComboBox.layoutProvider /template/ ContentControl#ContentPresenter">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Image file path" />
<TextBlock Classes="subtitle" FontSize="12" TextWrapping="Wrap" Text="{CompiledBinding ImagePath, TargetNullValue=None}" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
<Button
Classes="icon-button"
HorizontalAlignment="Right"
IsEnabled="{CompiledBinding !!ImagePath}"
ToolTip.Tip="Copy image file path to clipboard"
Click="ImagePathButton_OnClick">
<avalonia:MaterialIcon Kind="ContentCopy" Width="18" Height="18" />
</Button>
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Layout provider" />
<TextBlock Classes="subtitle" FontSize="12" Text="Choose between different ways to load a layout for this device." />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
<StackPanel.Styles>
<Style Selector="ComboBox.layoutProvider /template/ ContentControl#ContentPresenter">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Styles>
<ComboBox Classes="layoutProvider"
Width="150"
SelectedItem="{CompiledBinding SelectedLayoutProvider}"
ItemsSource="{CompiledBinding LayoutProviders}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
<StackPanel>
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Styles>
<ComboBox Classes="layoutProvider"
Width="150"
SelectedItem="{CompiledBinding SelectedLayoutProvider}"
ItemsSource="{CompiledBinding LayoutProviders}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
<StackPanel>
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
<TextBlock Classes="subtitle" Text="{CompiledBinding Description}" TextWrapping="Wrap" MaxWidth="350" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</Grid>
<TextBlock Classes="subtitle" Text="{CompiledBinding Description}" TextWrapping="Wrap" MaxWidth="350" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</Grid>
<ContentControl Content="{CompiledBinding SelectedLayoutProvider}" ClipToBounds="False" />
<ContentControl Content="{CompiledBinding SelectedLayoutProvider}" ClipToBounds="False" />
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Export current layout" />
<TextBlock Classes="subtitle" FontSize="12" Text="If there is a layout used, export that. Otherwise, export the LEDs present." />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal">
<Button HorizontalAlignment="Right" Content="Export" Command="{CompiledBinding ExportLayout}" />
</StackPanel>
</Grid>
</StackPanel>
<controls:HyperlinkButton
Grid.Row="1"
Content="Learn more about layouts on the wiki"
NavigateUri="https://wiki.artemis-rgb.com/en/guides/developer/layouts?mtm_campaign=artemis&amp;mtm_kwd=device-properties"
Margin="0 20"
HorizontalAlignment="Right"
VerticalAlignment="Bottom" />
</Grid>
</Border>
</ScrollViewer>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Export current layout" />
<TextBlock Classes="subtitle" FontSize="12" Text="If there is a layout used, export that. Otherwise, export the LEDs present." />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal">
<Button HorizontalAlignment="Right" Content="Export" Command="{CompiledBinding ExportLayout}" />
</StackPanel>
</Grid>
</StackPanel>
<controls:HyperlinkButton
Grid.Row="1"
Content="Learn more about layouts on the wiki"
NavigateUri="https://wiki.artemis-rgb.com/en/guides/developer/layouts?mtm_campaign=artemis&amp;mtm_kwd=device-properties"
Margin="0 20"
HorizontalAlignment="Right"
VerticalAlignment="Bottom" />
</Grid>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -34,7 +34,7 @@ public abstract partial class TreeItemViewModel : ActivatableViewModelBase
private RenderProfileElement? _currentProfileElement;
private ObservableAsPropertyHelper<bool>? _isFocused;
private TimeSpan _time;
[Notify] private bool _canPaste;
[Notify] private bool _isExpanded;
[Notify] private bool _isFlyoutOpen;
@ -100,7 +100,7 @@ public abstract partial class TreeItemViewModel : ActivatableViewModelBase
public ReactiveCommand<Unit, Unit> Paste { get; }
public ReactiveCommand<Unit, Unit> Delete { get; }
public abstract bool SupportsChildren { get; }
public async Task ShowBrokenStateExceptions()
{
if (ProfileElement == null)
@ -117,7 +117,7 @@ public abstract partial class TreeItemViewModel : ActivatableViewModelBase
return;
}
}
public void InsertElement(TreeItemViewModel elementViewModel, int targetIndex)
{
if (elementViewModel.Parent == this && Children.IndexOf(elementViewModel) == targetIndex)
@ -239,26 +239,22 @@ public abstract partial class TreeItemViewModel : ActivatableViewModelBase
await _windowService.ShowDialogAsync<LayerHintsDialogViewModel, bool>(layer);
await ProfileEditorService.SaveProfileAsync();
}
private void ExecuteApplyAdaptionHints()
{
if (ProfileElement is not Layer layer)
return;
ProfileEditorService.ExecuteCommand(new ApplyAdaptionHints(layer, _deviceService.EnabledDevices.ToList()));
}
private async void UpdateCanPaste(bool isFlyoutOpen)
{
string[] formats = await Shared.UI.Clipboard.GetFormatsAsync();
//diogotr7: This can be null on Linux sometimes. I'm not sure why.
if (formats == null!)
{
CanPaste = false;
return;
}
CanPaste = formats.Contains(ProfileElementExtensions.ClipboardDataFormat);
string[]? formats = await Shared.UI.Clipboard.GetFormatsAsync();
// Can be null on some platforms
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
CanPaste = formats != null && formats.Contains(ProfileElementExtensions.ClipboardDataFormat);
}
private bool GetIsFocused(ProfileEditorFocusMode focusMode, RenderProfileElement? currentProfileElement)

View File

@ -174,8 +174,11 @@ public partial class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, IT
private async void UpdateCanPaste(bool isFlyoutOpen)
{
string[] formats = await Shared.UI.Clipboard.GetFormatsAsync();
CanPaste = formats.Contains("Artemis.Keyframes");
string[]? formats = await Shared.UI.Clipboard.GetFormatsAsync();
// Can be null on some platforms
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
CanPaste = formats != null && formats.Contains("Artemis.Keyframes");
}
#endregion

View File

@ -19,7 +19,7 @@ public partial class RootView : ReactiveUserControl<RootViewModel>
{
try
{
Dispatcher.UIThread.Invoke(() => RootFrame.NavigateFromObject(viewModel));
RootFrame.NavigateFromObject(viewModel);
}
catch (Exception)
{

View File

@ -18,7 +18,7 @@ public partial class SettingsView : ReactiveUserControl<SettingsViewModel>
private void Navigate(ViewModelBase viewModel)
{
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
TabFrame.NavigateFromObject(viewModel);
}
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)

View File

@ -17,17 +17,13 @@ public partial class ReleasesTabView : ReactiveUserControl<ReleasesTabViewModel>
private void Navigate(ViewModelBase viewModel)
{
Dispatcher.UIThread.Invoke(() =>
try
{
try
{
ReleaseFrame.NavigateFromObject(viewModel);
}
catch (Exception)
{
// ignored
}
});
ReleaseFrame.NavigateFromObject(viewModel);
}
catch (Exception)
{
// ignored
}
}
}

View File

@ -43,7 +43,7 @@
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
<StackPanel>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Classes="h4 no-margin">Release info</TextBlock>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Release info</TextBlock>
<Panel Grid.Column="1" IsVisible="{CompiledBinding InstallationAvailable}">
<!-- Install progress -->
@ -124,7 +124,7 @@
</Grid>
<Grid Grid.Column="2" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Right">
<avalonia:MaterialIcon Kind="BoxOutline" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
<avalonia:MaterialIcon Kind="File" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">File size</TextBlock>
<TextBlock Grid.Column="1"
Grid.Row="1"
@ -149,5 +149,4 @@
</Grid>
</Border>
</Grid>
</UserControl>

View File

@ -13,71 +13,90 @@
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
<converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources>
<StackPanel>
<Panel>
<Border CornerRadius="6"
HorizontalAlignment="Left"
Margin="0 0 10 0"
Width="80"
Height="80"
ClipToBounds="True">
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
</Border>
<Button Classes="icon-button"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Command="{CompiledBinding CopyShareLink}"
ToolTip.Tip="Copy share link">
<avalonia:MaterialIcon Kind="ShareVariant" />
<Panel>
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}">
<Border Classes="skeleton-text" Margin="0 0 10 0" Width="80" Height="80"></Border>
<Border Classes="skeleton-text title" HorizontalAlignment="Stretch"/>
<Border Classes="skeleton-text" Width="120"/>
<Border Classes="skeleton-text" Width="140" Margin="0 8"/>
<Border Classes="skeleton-text" Width="80"/>
<Border Classes="card-separator" Margin="0 15 0 17"></Border>
<Border Classes="skeleton-text" Width="120"/>
<StackPanel Margin="0 10 0 0">
<Border Classes="skeleton-text" Width="160"/>
<Border Classes="skeleton-text" Width="160"/>
</StackPanel>
<Border Classes="skeleton-button"></Border>
</StackPanel>
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
<Panel>
<Border CornerRadius="6"
HorizontalAlignment="Left"
Margin="0 0 10 0"
Width="80"
Height="80"
ClipToBounds="True">
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
</Border>
<Button Classes="icon-button"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Command="{CompiledBinding CopyShareLink}"
ToolTip.Tip="Copy share link">
<avalonia:MaterialIcon Kind="ShareVariant" />
</Button>
</Panel>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}"
MaxLines="3"
TextTrimming="CharacterEllipsis"
Text="{CompiledBinding Entry.Name, FallbackValue=Title}"/>
<TextBlock Classes="subtitle" TextTrimming="CharacterEllipsis" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
<TextBlock Margin="0 8" TextWrapping="Wrap" Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}" />
<!-- Categories -->
<ItemsControl ItemsSource="{CompiledBinding Entry.Categories}" Margin="0 0 -8 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0 0 8 0">
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia:MaterialIcon>
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Border Classes="card-separator"></Border>
<TextBlock Margin="0 0 0 8">
<avalonia:MaterialIcon Kind="Downloads" />
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />
<Run>downloads</Run>
</TextBlock>
<TextBlock Classes="subtitle"
ToolTip.Tip="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Calendar" />
<Run>Created</Run>
<Run Text="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock>
<TextBlock Classes="subtitle"
ToolTip.Tip="{CompiledBinding UpdatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Update" />
<Run>Updated</Run>
<Run Text="{CompiledBinding UpdatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock>
<Button IsVisible="{CompiledBinding CanBeManaged}" Command="{CompiledBinding GoToManage}" Margin="0 10 0 0" HorizontalAlignment="Stretch">
Manage installation
</Button>
</Panel>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}"
MaxLines="3"
TextTrimming="CharacterEllipsis"
Text="{CompiledBinding Entry.Name, FallbackValue=Title }" />
<TextBlock Classes="subtitle" TextTrimming="CharacterEllipsis" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
<TextBlock Margin="0 8" TextWrapping="Wrap" Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}" />
<!-- Categories -->
<ItemsControl ItemsSource="{CompiledBinding Entry.Categories}" Margin="0 0 -8 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0 0 8 0">
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia:MaterialIcon>
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Border Classes="card-separator"></Border>
<TextBlock Margin="0 0 0 8">
<avalonia:MaterialIcon Kind="Downloads" />
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />
<Run>downloads</Run>
</TextBlock>
<TextBlock Classes="subtitle"
ToolTip.Tip="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Calendar" />
<Run>Created</Run>
<Run Text="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock>
<TextBlock Classes="subtitle"
ToolTip.Tip="{CompiledBinding UpdatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Update" />
<Run>Updated</Run>
<Run Text="{CompiledBinding UpdatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock>
</StackPanel>
</StackPanel>
</Panel>
</UserControl>

View File

@ -1,10 +1,11 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Entries.Details;
public partial class EntryInfoView : UserControl
public partial class EntryInfoView : ReactiveUserControl<EntryInfoViewModel>
{
public EntryInfoView()
{

View File

@ -1,28 +1,66 @@
using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Extensions;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Entries.Details;
public class EntryInfoViewModel : ViewModelBase
public partial class EntryInfoViewModel : ActivatableViewModelBase
{
private readonly IRouter _router;
private readonly INotificationService _notificationService;
public IEntryDetails Entry { get; }
public DateTimeOffset? UpdatedAt { get; }
public EntryInfoViewModel(IEntryDetails entry, INotificationService notificationService)
private readonly IWorkshopService _workshopService;
[Notify] private IEntryDetails? _entry;
[Notify] private DateTimeOffset? _updatedAt;
[Notify] private bool _canBeManaged;
public EntryInfoViewModel(IRouter router, INotificationService notificationService, IWorkshopService workshopService)
{
_router = router;
_notificationService = notificationService;
_workshopService = workshopService;
this.WhenActivated(d =>
{
Observable.FromEventPattern<InstalledEntry>(x => workshopService.OnInstalledEntrySaved += x, x => workshopService.OnInstalledEntrySaved -= x)
.StartWith([])
.Subscribe(_ => CanBeManaged = Entry != null && Entry.EntryType != EntryType.Profile && workshopService.GetInstalledEntry(Entry.Id) != null)
.DisposeWith(d);
});
}
public void SetEntry(IEntryDetails? entry)
{
Entry = entry;
UpdatedAt = Entry.LatestRelease?.CreatedAt ?? Entry.CreatedAt;
UpdatedAt = Entry != null && Entry.Releases.Any() ? Entry.Releases.Max(r => r.CreatedAt) : Entry?.CreatedAt;
CanBeManaged = Entry != null && Entry.EntryType != EntryType.Profile && _workshopService.GetInstalledEntry(Entry.Id) != null;
}
public async Task CopyShareLink()
{
if (Entry == null)
return;
await Shared.UI.Clipboard.SetTextAsync($"{WorkshopConstants.WORKSHOP_URL}/entries/{Entry.Id}/{StringUtilities.UrlFriendly(Entry.Name)}");
_notificationService.CreateNotification().WithTitle("Copied share link to clipboard.").Show();
}
public async Task GoToManage()
{
if (Entry == null)
return;
await _router.Navigate($"{Entry.GetEntryPath()}/manage");
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -2,18 +2,15 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:entries="clr-namespace:Artemis.UI.Screens.Workshop.Entries"
xmlns:tagsInput="clr-namespace:Artemis.UI.Shared.TagsInput;assembly=Artemis.UI.Shared"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:categories="clr-namespace:Artemis.UI.Screens.Workshop.Categories"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:details="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Details"
xmlns:controls="clr-namespace:Artemis.UI.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntrySpecificationsView"
x:DataType="details:EntrySpecificationsViewModel">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<Grid RowDefinitions="Auto,*,Auto">
<StackPanel>
<StackPanel.Styles>
<Styles>
@ -95,48 +92,9 @@
<tagsInput:TagsInput Tags="{CompiledBinding Tags}" />
</StackPanel>
<Grid Row="1" ColumnDefinitions="Auto,*">
<Label Grid.Column="0" Target="DescriptionEditor" Margin="0 28 0 0">Description</Label>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<CheckBox Name="SynchronizedScrolling" IsChecked="True" VerticalAlignment="Bottom">Synchronized scrolling</CheckBox>
<controls:HyperlinkButton
Margin="0 0 0 -20"
Content="Markdown supported"
NavigateUri="https://wiki.artemis-rgb.com/guides/user/markdown?mtm_campaign=artemis&amp;mtm_kwd=markdown-editor"
HorizontalAlignment="Right"/>
</StackPanel>
</Grid>
<Grid Grid.Row="2" ColumnDefinitions="*,Auto,*">
<Border Grid.Column="0" BorderThickness="1"
BorderBrush="{DynamicResource TextControlBorderBrush}"
CornerRadius="{DynamicResource ControlCornerRadius}"
Background="{DynamicResource TextControlBackground}"
Padding="{DynamicResource TextControlThemePadding}">
<avaloniaEdit:TextEditor
FontFamily="{StaticResource RobotoMono}"
FontSize="13"
Name="DescriptionEditor"
Document="{CompiledBinding MarkdownDocument}"
WordWrap="True" />
</Border>
<GridSplitter Grid.Column="1" Margin="5 0"></GridSplitter>
<Border Grid.Column="2" Classes="card-condensed">
<mdxaml:MarkdownScrollViewer Margin="5 0"
Name="DescriptionPreview"
Markdown="{CompiledBinding Description}"
MarkdownStyleName="FluentAvalonia"
SaveScrollValueWhenContentUpdated="True">
<mdxaml:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml" />
</mdxaml:MarkdownScrollViewer.Styles>
</mdxaml:MarkdownScrollViewer>
</Border>
</Grid>
<controls:SplitMarkdownEditor Grid.Row="1" Title="Description" Markdown="{CompiledBinding Description}"/>
<TextBlock Grid.Row="3"
<TextBlock Grid.Row="2"
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
Margin="2 8 0 0"
IsVisible="{CompiledBinding !DescriptionValid}">

View File

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

View File

@ -35,7 +35,6 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
[Notify] private string _summary = string.Empty;
[Notify] private string _description = string.Empty;
[Notify] private Bitmap? _iconBitmap;
[Notify] private TextDocument? _markdownDocument;
[Notify(Setter.Private)] private bool _iconChanged;
public EntrySpecificationsViewModel(IWorkshopClient workshopClient, IWindowService windowService)
@ -65,20 +64,7 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
_categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid);
_descriptionValid = descriptionRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.DescriptionValid);
this.WhenActivatedAsync(async d =>
{
// Load categories
await PopulateCategories();
MarkdownDocument = new TextDocument(new StringTextSource(Description));
MarkdownDocument.TextChanged += MarkdownDocumentOnTextChanged;
Disposable.Create(() =>
{
MarkdownDocument.TextChanged -= MarkdownDocumentOnTextChanged;
MarkdownDocument = null;
ClearIcon();
}).DisposeWith(d);
});
this.WhenActivatedAsync(async _ => await PopulateCategories());
}
public ReactiveCommand<Unit, Unit> SelectIcon { get; }
@ -92,12 +78,7 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
public bool DescriptionValid => _descriptionValid.Value;
public List<long> PreselectedCategories { get; set; } = new();
private void MarkdownDocumentOnTextChanged(object? sender, EventArgs e)
{
Description = MarkdownDocument?.Text ?? string.Empty;
}
private async Task ExecuteSelectIcon()
{
string[]? result = await _windowService.CreateOpenFileDialog()
@ -112,12 +93,6 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
IconChanged = true;
}
private void ClearIcon()
{
IconBitmap?.Dispose();
IconBitmap = null;
}
private async Task PopulateCategories()
{
IOperationResult<IGetCategoriesResult> categories = await _workshopClient.GetCategories.ExecuteAsync();

View File

@ -18,7 +18,7 @@ public partial class EntriesView : ReactiveUserControl<EntriesViewModel>
private void Navigate(ViewModelBase viewModel)
{
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
TabFrame.NavigateFromObject(viewModel);
}
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)

View File

@ -53,8 +53,8 @@ public partial class EntriesViewModel : RoutableHostScreen<RoutableScreen>
public void GoBack()
{
if (ViewingDetails)
_router.GoBack();
if (ViewingDetails && SelectedTab != null)
_router.Navigate(SelectedTab.Path);
else
_router.Navigate("workshop");
}

View File

@ -79,11 +79,11 @@
<!-- Install state -->
<StackPanel Grid.Column="2" Grid.Row="1" Margin="0 0 4 0" HorizontalAlignment="Right" VerticalAlignment="Bottom" IsVisible="{CompiledBinding IsInstalled}">
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding !UpdateAvailable}">
<avalonia:MaterialIcon Kind="CheckCircle" Foreground="{DynamicResource SystemAccentColorLight1}"/>
<avalonia:MaterialIcon Kind="CheckCircle" Foreground="{DynamicResource SystemAccentColorLight1}" Width="20" Height="20"/>
<Run>installed</Run>
</TextBlock>
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding UpdateAvailable}">
<avalonia:MaterialIcon Kind="Update" Foreground="{DynamicResource SystemAccentColorLight1}"/>
<avalonia:MaterialIcon Kind="Update" Foreground="{DynamicResource SystemAccentColorLight1}" Width="20" Height="20"/>
<Run>update available</Run>
</TextBlock>
</StackPanel>

View File

@ -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>

View File

@ -1,38 +1,30 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
using Artemis.UI.Shared.Routing;
using Avalonia.Controls;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
namespace Artemis.UI.Screens.Workshop.Entries.List;
public partial class LayoutListView : ReactiveUserControl<LayoutListViewModel>
public partial class EntryListView : ReactiveUserControl<EntryListViewModel>
{
public LayoutListView()
public EntryListView()
{
InitializeComponent();
EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch();
this.WhenActivated(d =>
{
UpdateEntriesPerFetch();
ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d);
});
this.WhenActivated(_ => UpdateEntriesPerFetch());
}
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
if (ViewModel == null)
return;
// When near the bottom of EntriesScrollViewer, call FetchMore on the view model
if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100)
ViewModel?.FetchMore(CancellationToken.None);
}
ViewModel.FetchMore(CancellationToken.None);
private void Navigate(RoutableScreen viewModel)
{
Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle);
ViewModel.ScrollOffset = EntriesScrollViewer.Offset;
}
private void UpdateEntriesPerFetch()

View File

@ -6,6 +6,7 @@ using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Extensions;
using Artemis.UI.Screens.Workshop.Categories;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
@ -15,29 +16,28 @@ using DynamicData;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using StrawberryShake;
using Vector = Avalonia.Vector;
namespace Artemis.UI.Screens.Workshop.Entries.List;
public abstract partial class EntryListViewModel : RoutableHostScreen<RoutableScreen>
public partial class EntryListViewModel : RoutableScreen
{
private readonly SourceList<IEntrySummary> _entries = new();
private readonly INotificationService _notificationService;
private readonly IWorkshopClient _workshopClient;
private readonly string _route;
private IGetEntriesv2_EntriesV2_PageInfo? _currentPageInfo;
[Notify] private bool _initializing = true;
[Notify] private bool _fetchingMore;
[Notify] private int _entriesPerFetch;
[Notify] private Vector _scrollOffset;
protected EntryListViewModel(string route,
IWorkshopClient workshopClient,
protected EntryListViewModel(IWorkshopClient workshopClient,
CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService,
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
{
_route = route;
_workshopClient = workshopClient;
_notificationService = notificationService;
@ -50,37 +50,31 @@ public abstract partial class EntryListViewModel : RoutableHostScreen<RoutableSc
.Subscribe();
Entries = entries;
// Respond to filter query input changes
this.WhenActivated(d =>
{
// Respond to filter query input changes
InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d);
CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => Reset()).DisposeWith(d);
});
// Load entries when the view model is first activated
this.WhenActivatedAsync(async _ =>
{
if (_entries.Count == 0)
{
await Task.Delay(250);
await FetchMore(CancellationToken.None);
Initializing = false;
}
});
}
public CategoriesViewModel CategoriesViewModel { get; }
public EntryListInputViewModel InputViewModel { get; }
public EntryType? EntryType { get; set; }
public ReadOnlyObservableCollection<EntryListItemViewModel> Entries { get; }
public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
if (_entries.Count == 0)
{
await Task.Delay(250, cancellationToken);
await FetchMore(cancellationToken);
Initializing = false;
}
}
public override Task OnClosing(NavigationArguments args)
{
// Clear search if not navigating to a child
if (!args.Path.StartsWith(_route))
InputViewModel.ClearLastSearch();
return base.OnClosing(args);
}
public async Task FetchMore(CancellationToken cancellationToken)
{
if (FetchingMore || _currentPageInfo != null && !_currentPageInfo.HasNextPage)
@ -119,12 +113,19 @@ public abstract partial class EntryListViewModel : RoutableHostScreen<RoutableSc
}
}
protected virtual EntryFilterInput GetFilter()
private EntryFilterInput GetFilter()
{
return new EntryFilterInput {And = CategoriesViewModel.CategoryFilters};
return new EntryFilterInput
{
And =
[
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType}},
..CategoriesViewModel.CategoryFilters ?? []
]
};
}
protected virtual IReadOnlyList<EntrySortInput> GetSort()
private IReadOnlyList<EntrySortInput> GetSort()
{
// Sort by created at
if (InputViewModel.SortBy == 1)

View File

@ -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>

View File

@ -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()
}
};
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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()
}
};
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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()
}
};
}
}

View File

@ -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>

View File

@ -0,0 +1,11 @@
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.EntryReleases;
public partial class EntryReleaseItemView : ReactiveUserControl<EntryReleaseItemViewModel>
{
public EntryReleaseItemView()
{
InitializeComponent();
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Extensions;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.EntryReleases;
public partial class EntryReleasesViewModel : ActivatableViewModelBase
{
private readonly IRouter _router;
[Notify] private EntryReleaseItemViewModel? _selectedRelease;
public EntryReleasesViewModel(IEntryDetails entry, IRouter router, Func<IRelease, EntryReleaseItemViewModel> getEntryReleaseItemViewModel)
{
_router = router;
Entry = entry;
Releases = Entry.Releases.OrderByDescending(r => r.CreatedAt).Take(5).Select(r => getEntryReleaseItemViewModel(r)).ToList();
this.WhenActivated(d =>
{
router.CurrentPath.Subscribe(p =>
SelectedRelease = p != null && p.StartsWith($"{Entry.GetEntryPath()}/releases") && float.TryParse(p.Split('/').Last(), out float releaseId)
? Releases.FirstOrDefault(r => r.Release.Id == releaseId)
: null)
.DisposeWith(d);
this.WhenAnyValue(vm => vm.SelectedRelease)
.WhereNotNull()
.Subscribe(s => _router.Navigate($"{Entry.GetEntryPath()}/releases/{s.Release.Id}"))
.DisposeWith(d);
});
}
public IEntryDetails Entry { get; }
public List<EntryReleaseItemViewModel> Releases { get; }
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -3,7 +3,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView"
x:DataType="layout:LayoutDetailsViewModel">
@ -12,21 +13,17 @@
<Border Classes="card" VerticalAlignment="Top">
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
</Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.LatestRelease, Converter={x:Static ObjectConverters.IsNotNull}}">
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count, FallbackValue=False}">
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
</Border>
</StackPanel>
<ScrollViewer Grid.Row="1" Grid.Column="1">
<StackPanel Margin="10 0" Spacing="10">
<Border Classes="card">
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
<mdxaml:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml" />
</mdxaml:MarkdownScrollViewer.Styles>
</mdxaml:MarkdownScrollViewer>
</Border>
</StackPanel>
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" Margin="10 0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</ScrollViewer>
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" />

View File

@ -1,4 +1,7 @@
using System;
using System.Reactive.Disposables;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Layout;
@ -7,5 +10,9 @@ public partial class LayoutDetailsView : ReactiveUserControl<LayoutDetailsViewMo
public LayoutDetailsView()
{
InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d));
}
}

View File

@ -1,87 +1,64 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Screens.Workshop.Entries.Details;
using Artemis.UI.Screens.Workshop.Layout.Dialogs;
using Artemis.UI.Screens.Workshop.EntryReleases;
using Artemis.UI.Screens.Workshop.Parameters;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Layout;
public partial class LayoutDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
public partial class LayoutDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters>
{
private readonly IWorkshopClient _client;
private readonly IDeviceService _deviceService;
private readonly IWindowService _windowService;
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
private readonly LayoutDescriptionViewModel _layoutDescriptionViewModel;
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
[Notify] private IEntryDetails? _entry;
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
public LayoutDetailsViewModel(IWorkshopClient client,
IDeviceService deviceService,
IWindowService windowService,
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
LayoutDescriptionViewModel layoutDescriptionViewModel,
EntryInfoViewModel entryInfoViewModel,
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
{
_client = client;
_deviceService = deviceService;
_windowService = windowService;
_getEntryInfoViewModel = getEntryInfoViewModel;
_layoutDescriptionViewModel = layoutDescriptionViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel;
RecycleScreen = false;
EntryInfoViewModel = entryInfoViewModel;
}
public override RoutableScreen DefaultScreen => _layoutDescriptionViewModel;
public EntryInfoViewModel EntryInfoViewModel { get; }
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
await GetEntry(parameters.EntryId, cancellationToken);
if (Entry?.Id != parameters.EntryId)
await GetEntry(parameters.EntryId, cancellationToken);
}
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
{
Task grace = Task.Delay(300, cancellationToken);
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
if (result.IsErrorResult())
return;
// Let the UI settle to avoid lag when deep linking
await grace;
Entry = result.Data?.Entry;
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
EntryInfoViewModel.SetEntry(Entry);
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
if (EntryReleasesViewModel != null)
EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished;
}
private async Task OnInstallationFinished(InstalledEntry installedEntry)
{
// Find compatible devices
ArtemisLayout layout = new(Path.Combine(installedEntry.GetReleaseDirectory().FullName, "layout.xml"));
List<ArtemisDevice> devices = _deviceService.Devices.Where(d => d.RgbDevice.DeviceInfo.DeviceType == layout.RgbLayout.Type).ToList();
// If any are found, offer to apply
if (devices.Any())
{
await _windowService.CreateContentDialog()
.WithTitle("Apply layout to devices")
.WithViewModel(out DeviceSelectionDialogViewModel vm, devices, installedEntry)
.WithCloseButtonText(null)
.HavingPrimaryButton(b => b.WithText("Continue").WithCommand(vm.Apply))
.ShowAsync();
}
_layoutDescriptionViewModel.Entry = Entry;
}
}

View 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>

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,46 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
xmlns:surfaceEditor="clr-namespace:Artemis.UI.Screens.SurfaceEditor"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutManageView"
x:DataType="layout:LayoutManageViewModel">
<Border Classes="card" VerticalAlignment="Top">
<StackPanel>
<Grid ColumnDefinitions="Auto,*,Auto">
<Button Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Classes="icon-button" Command="{CompiledBinding Close}">
<avalonia:MaterialIcon Kind="ArrowBack" />
</Button>
<TextBlock Grid.Row="0" Grid.Column="1" Theme="{StaticResource SubtitleTextBlockStyle}">Manage layout</TextBlock>
</Grid>
<Border Classes="card-separator" />
<TextBlock IsVisible="{CompiledBinding !Devices.Count}">
This layout is made for devices of type
<Run FontWeight="Bold" Text="{CompiledBinding Layout.RgbLayout.Type}"/>.<LineBreak/>
Unfortunately, none were detected.
</TextBlock>
<StackPanel IsVisible="{CompiledBinding Devices.Count}">
<TextBlock>
Select the devices on which you would like to apply the downloaded layout.
</TextBlock>
<ItemsControl Name="EffectDescriptorsList" ItemsSource="{CompiledBinding Devices}" Margin="0 10">
<ItemsControl.DataTemplates>
<DataTemplate DataType="{x:Type surfaceEditor:ListDeviceViewModel}">
<CheckBox IsChecked="{CompiledBinding IsSelected}">
<TextBlock Text="{CompiledBinding Device.RgbDevice.DeviceInfo.DeviceName}"></TextBlock>
</CheckBox>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
<Button Command="{CompiledBinding Apply}">Apply</Button>
</StackPanel>
</StackPanel>
</Border>
</UserControl>

View File

@ -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();
}
}

View 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);
}
}
}

View File

@ -1,85 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:library="clr-namespace:Artemis.UI.Screens.Workshop.Library"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.SubmissionDetailView"
x:DataType="library:SubmissionDetailViewModel">
<UserControl.Resources>
<converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources>
<Grid ColumnDefinitions="300,*,300" RowDefinitions="*, Auto">
<StackPanel Grid.Column="0" Grid.RowSpan="2" Spacing="10">
<Border Classes="card" VerticalAlignment="Top" Margin="0 0 10 0">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Management</TextBlock>
<Border Classes="card-separator" />
<TextBlock Margin="0 0 0 8">
<avalonia:MaterialIcon Kind="Downloads" />
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />
<Run>downloads</Run>
</TextBlock>
<TextBlock Classes="subtitle"
ToolTip.Tip="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Calendar" />
<Run>Created</Run>
<Run Text="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock>
<Border Classes="card-separator" />
<StackPanel Spacing="5">
<Button HorizontalAlignment="Stretch" Command="{CompiledBinding CreateRelease}">
Create new release
</Button>
<Button Classes="danger" HorizontalAlignment="Stretch" Command="{CompiledBinding DeleteSubmission}">
Delete submission
</Button>
</StackPanel>
</StackPanel>
</Border>
<controls:HyperlinkButton Command="{CompiledBinding ViewWorkshopPage}" HorizontalAlignment="Center">
View workshop page
</controls:HyperlinkButton>
</StackPanel>
<ContentControl Grid.Column="1" Grid.Row="0" Content="{CompiledBinding EntrySpecificationsViewModel}"></ContentControl>
<Border Grid.Column="2" Grid.Row="0" Classes="card" Margin="10 0 0 0">
<Grid RowDefinitions="*,Auto">
<ScrollViewer Grid.Row="0" Classes="with-padding" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{CompiledBinding Images}">
<ItemsControl.Styles>
<Styles>
<Style Selector="ItemsControl > ContentPresenter">
<Setter Property="Margin" Value="0 0 0 10"></Setter>
</Style>
<Style Selector="ItemsControl > ContentPresenter:nth-last-child(1)">
<Setter Property="Margin" Value="0 0 0 0"></Setter>
</Style>
</Styles>
</ItemsControl.Styles>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
<Button Grid.Row="1" HorizontalAlignment="Stretch" Command="{CompiledBinding AddImage}">Add image</Button>
</Grid>
</Border>
<StackPanel Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="1" HorizontalAlignment="Right" Spacing="5" Orientation="Horizontal" Margin="0 10 0 0">
<Button Command="{CompiledBinding DiscardChanges}">Discard changes</Button>
<Button Command="{CompiledBinding SaveChanges}">Save</Button>
</StackPanel>
</Grid>
</UserControl>

View File

@ -1,11 +0,0 @@
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Library;
public partial class SubmissionDetailView : ReactiveUserControl<SubmissionDetailViewModel>
{
public SubmissionDetailView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,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>

View File

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

View File

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

View File

@ -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>

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -0,0 +1,11 @@
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Library;
public partial class SubmissionReleaseView : ReactiveUserControl<SubmissionReleaseViewModel>
{
public SubmissionReleaseView()
{
InitializeComponent();
}
}

View File

@ -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;
}
}

View File

@ -18,9 +18,9 @@ public partial class WorkshopLibraryView : ReactiveUserControl<WorkshopLibraryVi
private void Navigate(ViewModelBase viewModel)
{
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
TabFrame.NavigateFromObject(viewModel);
}
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
{
ViewModel?.GoBack();

View File

@ -53,7 +53,7 @@ public partial class WorkshopLibraryViewModel : RoutableHostScreen<RoutableScree
public void GoBack()
{
if (ViewingDetails)
_router.GoBack();
_router.Navigate("workshop/library/submissions");
else
_router.Navigate("workshop");
}

View File

@ -0,0 +1,6 @@
namespace Artemis.UI.Screens.Workshop.Parameters;
public class ReleaseDetailParameters
{
public long ReleaseId { get; set; }
}

View File

@ -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>

View File

@ -1,11 +0,0 @@
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Plugins.Dialogs;
public partial class PluginDialogView : ReactiveUserControl<PluginDialogViewModel>
{
public PluginDialogView()
{
InitializeComponent();
}
}

View File

@ -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; }
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -2,9 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginDetailsView"
x:DataType="plugins:PluginDetailsViewModel">
@ -15,47 +16,44 @@
</Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.PluginInfo, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel>
<TextBlock>Admin required</TextBlock>
<TextBlock Text="Yes" IsVisible="{CompiledBinding Entry.PluginInfo.RequiresAdmin}" />
<TextBlock Text="No" IsVisible="{CompiledBinding !Entry.PluginInfo.RequiresAdmin}" />
<TextBlock Margin="0 15 0 5">Supported platforms</TextBlock>
<StackPanel Orientation="Horizontal" Spacing="10">
<avalonia:MaterialIcon Kind="MicrosoftWindows" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsWindows}" />
<avalonia:MaterialIcon Kind="Linux" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsLinux}" />
<avalonia:MaterialIcon Kind="Apple" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsOSX}" />
<Panel>
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}">
<Border Width="110" Classes="skeleton-text"></Border>
<Border Width="35" Classes="skeleton-text"></Border>
<Border Margin="0 16 0 3" Width="130" Classes="skeleton-text"></Border>
<Border Width="60" Classes="skeleton-text"></Border>
</StackPanel>
</StackPanel>
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock>Admin required</TextBlock>
<TextBlock Text="Yes" IsVisible="{CompiledBinding Entry.PluginInfo.RequiresAdmin}" />
<TextBlock Text="No" IsVisible="{CompiledBinding !Entry.PluginInfo.RequiresAdmin}" />
<TextBlock Margin="0 15 0 5">Supported platforms</TextBlock>
<StackPanel Orientation="Horizontal" Spacing="10">
<avalonia:MaterialIcon Kind="MicrosoftWindows" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsWindows}" />
<avalonia:MaterialIcon Kind="Linux" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsLinux}" />
<avalonia:MaterialIcon Kind="Apple" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsOSX}" />
</StackPanel>
</StackPanel>
</Panel>
</Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.LatestRelease, Converter={x:Static ObjectConverters.IsNotNull}}">
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count, FallbackValue=False}">
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
</Border>
</StackPanel>
<ScrollViewer Grid.Row="1" Grid.Column="1">
<StackPanel Margin="10 0" Spacing="10">
<Border Classes="card">
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
<mdxaml:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml" />
</mdxaml:MarkdownScrollViewer.Styles>
</mdxaml:MarkdownScrollViewer>
</Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Dependants, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Used by these profiles</TextBlock>
<Border Classes="card-separator" />
<ScrollViewer>
<ItemsControl ItemsSource="{CompiledBinding Dependants}"></ItemsControl>
</ScrollViewer>
</StackPanel>
</Border>
</StackPanel>
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" Margin="10 0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</ScrollViewer>
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" />
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count, FallbackValue=False}" Content="{CompiledBinding EntryImagesViewModel}" />
</Grid>
</UserControl>

View File

@ -1,4 +1,7 @@
using System;
using System.Reactive.Disposables;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Plugins;
@ -7,5 +10,9 @@ public partial class PluginDetailsView : ReactiveUserControl<PluginDetailsViewMo
public PluginDetailsView()
{
InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d));
}
}

View File

@ -1,104 +1,68 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Screens.Workshop.Entries.Details;
using Artemis.UI.Screens.Workshop.Entries.List;
using Artemis.UI.Screens.Workshop.EntryReleases;
using Artemis.UI.Screens.Workshop.Parameters;
using Artemis.UI.Screens.Workshop.Plugins.Dialogs;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Models;
using PropertyChanged.SourceGenerator;
using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Plugins;
public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
public partial class PluginDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters>
{
private readonly IWorkshopClient _client;
private readonly IWindowService _windowService;
private readonly IPluginManagementService _pluginManagementService;
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
private readonly PluginDescriptionViewModel _pluginDescriptionViewModel;
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
private readonly Func<IEntrySummary, EntryListItemViewModel> _getEntryListViewModel;
[Notify] private IGetPluginEntryById_Entry? _entry;
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
[Notify] private ReadOnlyObservableCollection<EntryListItemViewModel>? _dependants;
public PluginDetailsViewModel(IWorkshopClient client,
IWindowService windowService,
IPluginManagementService pluginManagementService,
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
PluginDescriptionViewModel pluginDescriptionViewModel,
EntryInfoViewModel entryInfoViewModel,
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel,
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
{
_client = client;
_windowService = windowService;
_pluginManagementService = pluginManagementService;
_getEntryInfoViewModel = getEntryInfoViewModel;
_pluginDescriptionViewModel = pluginDescriptionViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel;
_getEntryListViewModel = getEntryListViewModel;
EntryInfoViewModel = entryInfoViewModel;
RecycleScreen = false;
}
public override RoutableScreen DefaultScreen => _pluginDescriptionViewModel;
public EntryInfoViewModel EntryInfoViewModel { get; }
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
await GetEntry(parameters.EntryId, cancellationToken);
if (Entry?.Id != parameters.EntryId)
await GetEntry(parameters.EntryId, cancellationToken);
}
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
{
Task grace = Task.Delay(300, cancellationToken);
IOperationResult<IGetPluginEntryByIdResult> result = await _client.GetPluginEntryById.ExecuteAsync(entryId, cancellationToken);
if (result.IsErrorResult())
return;
// Let the UI settle to avoid lag when deep linking
await grace;
Entry = result.Data?.Entry;
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
EntryInfoViewModel.SetEntry(Entry);
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
if (EntryReleasesViewModel != null)
{
EntryReleasesViewModel.OnInstallationStarted = OnInstallationStarted;
EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished;
}
IReadOnlyList<IEntrySummary>? dependants = (await _client.GetDependantEntries.ExecuteAsync(entryId, 0, 25, cancellationToken)).Data?.Entries?.Items;
Dependants = dependants != null && dependants.Any()
? new ReadOnlyObservableCollection<EntryListItemViewModel>(new ObservableCollection<EntryListItemViewModel>(dependants.Select(_getEntryListViewModel)))
: null;
}
private async Task<bool> OnInstallationStarted(IEntryDetails entryDetails)
{
bool confirm = await _windowService.ShowConfirmContentDialog(
"Installing plugin",
$"You are about to install version {entryDetails.LatestRelease?.Version} of {entryDetails.Name}. \r\n\r\n" +
"Plugins are NOT verified by Artemis and could harm your PC, if you have doubts about a plugin please ask on Discord!",
"I trust this plugin, install it"
);
return !confirm;
}
private async Task OnInstallationFinished(InstalledEntry installedEntry)
{
if (!installedEntry.TryGetMetadata("PluginId", out Guid pluginId))
return;
Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
if (plugin == null)
return;
await _windowService.CreateContentDialog().WithTitle("Manage plugin").WithViewModel(out PluginDialogViewModel _, plugin).WithFullScreen().ShowAsync();
await _pluginDescriptionViewModel.SetEntry(Entry, cancellationToken);
}
}

View 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>

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,33 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginManageView"
x:DataType="plugins:PluginManageViewModel">
<Border Classes="card" VerticalAlignment="Top">
<StackPanel>
<Grid ColumnDefinitions="Auto,*,Auto">
<Button Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Classes="icon-button" Command="{CompiledBinding Close}">
<avalonia:MaterialIcon Kind="ArrowBack" />
</Button>
<TextBlock Grid.Row="0" Grid.Column="1" Theme="{StaticResource SubtitleTextBlockStyle}">Manage plugin</TextBlock>
</Grid>
<Border Classes="card-separator" />
<Grid ColumnDefinitions="4*,5*" Height="160">
<ContentControl Grid.Column="0" Content="{CompiledBinding PluginViewModel}" />
<Border Grid.Column="1" BorderBrush="{DynamicResource ButtonBorderBrush}" BorderThickness="1 0 0 0" Margin="10 0 0 0" Padding="10 0 0 0">
<Grid RowDefinitions="Auto,*">
<TextBlock Classes="h5">Plugin features</TextBlock>
<ListBox Grid.Row="1" MaxHeight="135" ItemsSource="{CompiledBinding PluginFeatures}" />
</Grid>
</Border>
</Grid>
</StackPanel>
</Border>
</UserControl>

View File

@ -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();
}
}

View File

@ -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)));
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -3,7 +3,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView"
x:DataType="profile:ProfileDetailsViewModel">
@ -12,34 +13,19 @@
<Border Classes="card" VerticalAlignment="Top">
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
</Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.LatestRelease, Converter={x:Static ObjectConverters.IsNotNull}}">
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count, FallbackValue=False}">
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
</Border>
</StackPanel>
<ScrollViewer Grid.Row="1" Grid.Column="1">
<StackPanel Margin="10 0" Spacing="10">
<Border Classes="card">
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
<mdxaml:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml" />
</mdxaml:MarkdownScrollViewer.Styles>
</mdxaml:MarkdownScrollViewer>
</Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Dependencies, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Required plugins</TextBlock>
<Border Classes="card-separator" />
<ScrollViewer>
<ItemsControl ItemsSource="{CompiledBinding Dependencies}"></ItemsControl>
</ScrollViewer>
</StackPanel>
</Border>
</StackPanel>
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" Margin="10 0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</ScrollViewer>
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" />
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count, FallbackValue=False}" Content="{CompiledBinding EntryImagesViewModel}" />
</Grid>
</UserControl>

View File

@ -1,4 +1,7 @@
using System;
using System.Reactive.Disposables;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Profile;
@ -7,5 +10,9 @@ public partial class ProfileDetailsView : ReactiveUserControl<ProfileDetailsView
public ProfileDetailsView()
{
InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d));
}
}

Some files were not shown because too many files have changed in this diff Show More