1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 21:38:38 +00:00

Merge branch 'development' into feature/workshop

This commit is contained in:
Robert 2023-07-06 18:41:59 +02:00
commit 229d93901b
76 changed files with 2016 additions and 854 deletions

View File

@ -241,7 +241,7 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
return _stops[^1].Color; return _stops[^1].Color;
//find the first stop after the position //find the first stop after the position
int stop2Index = 0; int stop2Index = -1;
for (int i = 0; i < _stops.Count; i++) for (int i = 0; i < _stops.Count; i++)
{ {

View File

@ -17,10 +17,10 @@
<TrayIcon Icon="avares://Artemis.UI/Assets/Images/Logo/application.ico" ToolTipText="Artemis" Command="{CompiledBinding OpenScreen}" CommandParameter="Home"> <TrayIcon Icon="avares://Artemis.UI/Assets/Images/Logo/application.ico" ToolTipText="Artemis" Command="{CompiledBinding OpenScreen}" CommandParameter="Home">
<TrayIcon.Menu> <TrayIcon.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="Home" Command="{CompiledBinding OpenScreen}" CommandParameter="Home" /> <NativeMenuItem Header="Home" Command="{CompiledBinding OpenScreen}" CommandParameter="home" />
<!-- <NativeMenuItem Header="Workshop" Command="{CompiledBinding OpenScreen}" CommandParameter="Workshop" /> --> <!-- <NativeMenuItem Header="Workshop" Command="{CompiledBinding OpenScreen}" CommandParameter="workshop" /> -->
<NativeMenuItem Header="Surface Editor" Command="{CompiledBinding OpenScreen}" CommandParameter="Surface Editor" /> <NativeMenuItem Header="Surface Editor" Command="{CompiledBinding OpenScreen}" CommandParameter="surface-editor" />
<NativeMenuItem Header="Settings" Command="{CompiledBinding OpenScreen}" CommandParameter="Settings" /> <NativeMenuItem Header="Settings" Command="{CompiledBinding OpenScreen}" CommandParameter="settings" />
<NativeMenuItemSeparator /> <NativeMenuItemSeparator />
<NativeMenuItem Header="Debugger" Command="{CompiledBinding OpenDebugger}" /> <NativeMenuItem Header="Debugger" Command="{CompiledBinding OpenDebugger}" />
<NativeMenuItem Header="Exit" Command="{CompiledBinding Exit}" /> <NativeMenuItem Header="Exit" Command="{CompiledBinding Exit}" />

View File

@ -17,10 +17,10 @@
<TrayIcon Icon="avares://Artemis.UI/Assets/Images/Logo/application.ico" ToolTipText="Artemis" Command="{CompiledBinding OpenScreen}" CommandParameter="Home"> <TrayIcon Icon="avares://Artemis.UI/Assets/Images/Logo/application.ico" ToolTipText="Artemis" Command="{CompiledBinding OpenScreen}" CommandParameter="Home">
<TrayIcon.Menu> <TrayIcon.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="Home" Command="{CompiledBinding OpenScreen}" CommandParameter="Home" /> <NativeMenuItem Header="Home" Command="{CompiledBinding OpenScreen}" CommandParameter="home" />
<!-- <NativeMenuItem Header="Workshop" Command="{CompiledBinding OpenScreen}" CommandParameter="Workshop" /> --> <!-- <NativeMenuItem Header="Workshop" Command="{CompiledBinding OpenScreen}" CommandParameter="workshop" /> -->
<NativeMenuItem Header="Surface Editor" Command="{CompiledBinding OpenScreen}" CommandParameter="Surface Editor" /> <NativeMenuItem Header="Surface Editor" Command="{CompiledBinding OpenScreen}" CommandParameter="surface-editor" />
<NativeMenuItem Header="Settings" Command="{CompiledBinding OpenScreen}" CommandParameter="Settings" /> <NativeMenuItem Header="Settings" Command="{CompiledBinding OpenScreen}" CommandParameter="settings" />
<NativeMenuItemSeparator /> <NativeMenuItemSeparator />
<NativeMenuItem Header="Debugger" Command="{CompiledBinding OpenDebugger}" /> <NativeMenuItem Header="Debugger" Command="{CompiledBinding OpenDebugger}" />
<NativeMenuItem Header="Exit" Command="{CompiledBinding Exit}" /> <NativeMenuItem Header="Exit" Command="{CompiledBinding Exit}" />

View File

@ -2,6 +2,9 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=exceptions/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=exceptions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=routing_005Croutable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=routing_005Croute/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=routing_005Crouter/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindow/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindow/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindows/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindows/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindowservice/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindowservice/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Shared.Events; using Artemis.UI.Shared.Events;
@ -26,6 +27,7 @@ namespace Artemis.UI.Shared;
/// </summary> /// </summary>
public class DeviceVisualizer : Control public class DeviceVisualizer : Control
{ {
internal static readonly Dictionary<ArtemisDevice, RenderTargetBitmap?> BitmapCache = new();
private readonly ICoreService _coreService; private readonly ICoreService _coreService;
private readonly List<DeviceVisualizerLed> _deviceVisualizerLeds; private readonly List<DeviceVisualizerLed> _deviceVisualizerLeds;
@ -160,7 +162,7 @@ public class DeviceVisualizer : Control
return geometry.Bounds; return geometry.Bounds;
} }
private void OnFrameRendered(object? sender, FrameRenderedEventArgs e) private void OnFrameRendered(object? sender, FrameRenderedEventArgs e)
{ {
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
@ -195,12 +197,14 @@ public class DeviceVisualizer : Control
private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e) private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{ {
Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background); if (Device != null)
BitmapCache.Remove(Device);
Dispatcher.UIThread.Invoke(SetupForDevice, DispatcherPriority.Background);
} }
private void DeviceUpdated(object? sender, EventArgs e) private void DeviceUpdated(object? sender, EventArgs e)
{ {
Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background); Dispatcher.UIThread.Invoke(SetupForDevice, DispatcherPriority.Background);
} }
#region Properties #region Properties
@ -242,9 +246,6 @@ public class DeviceVisualizer : Control
/// <inheritdoc /> /// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{ {
_deviceImage?.Dispose();
_deviceImage = null;
if (Device != null) if (Device != null)
{ {
Device.RgbDevice.PropertyChanged -= DevicePropertyChanged; Device.RgbDevice.PropertyChanged -= DevicePropertyChanged;
@ -270,7 +271,7 @@ public class DeviceVisualizer : Control
base.OnDetachedFromLogicalTree(e); base.OnDetachedFromLogicalTree(e);
} }
private void SetupForDevice() private async Task SetupForDevice()
{ {
lock (_deviceVisualizerLeds) lock (_deviceVisualizerLeds)
{ {
@ -302,46 +303,47 @@ public class DeviceVisualizer : Control
// Load the device main image on a background thread // Load the device main image on a background thread
ArtemisDevice? device = Device; ArtemisDevice? device = Device;
Dispatcher.UIThread.Post(() => try
{ {
try _deviceImage = await Task.Run(() => GetDeviceImage(device));
{ }
if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath)) catch (Exception e)
{ {
_deviceImage?.Dispose(); // ignored
_deviceImage = null; }
return;
}
// Create a bitmap that'll be used to render the device and LED images just once InvalidateMeasure();
// Render 4 times the actual size of the device to make sure things look sharp when zoomed in _loading = false;
RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) device.RgbDevice.ActualSize.Width * 2, (int) device.RgbDevice.ActualSize.Height * 2)); }
using DrawingContext context = renderTargetBitmap.CreateDrawingContext(); private RenderTargetBitmap? GetDeviceImage(ArtemisDevice device)
using Bitmap bitmap = new(device.Layout.Image.LocalPath); {
using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(renderTargetBitmap.PixelSize); if (BitmapCache.TryGetValue(device, out RenderTargetBitmap? existingBitmap))
return existingBitmap;
context.DrawImage(scaledBitmap, new Rect(scaledBitmap.Size)); if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath))
lock (_deviceVisualizerLeds) {
{ BitmapCache[device] = null;
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) return null;
deviceVisualizerLed.DrawBitmap(context, 2 * Device.Scale); }
}
_deviceImage?.Dispose(); // Create a bitmap that'll be used to render the device and LED images just once
_deviceImage = renderTargetBitmap; // Render 4 times the actual size of the device to make sure things look sharp when zoomed in
RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) device.RgbDevice.ActualSize.Width * 2, (int) device.RgbDevice.ActualSize.Height * 2));
InvalidateMeasure(); using DrawingContext context = renderTargetBitmap.CreateDrawingContext();
} using Bitmap bitmap = new(device.Layout.Image.LocalPath);
catch (Exception) using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(renderTargetBitmap.PixelSize);
{
// ignored context.DrawImage(scaledBitmap, new Rect(scaledBitmap.Size));
} lock (_deviceVisualizerLeds)
finally {
{ foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
_loading = false; deviceVisualizerLed.DrawBitmap(context, 2 * device.Scale);
} }
});
BitmapCache[device] = renderTargetBitmap;
return renderTargetBitmap;
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -48,11 +48,14 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable
} }
else else
{ {
Stream? stream = ConfigurationIcon.GetIconStream(); Dispatcher.UIThread.Post(() =>
if (stream == null) {
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; Stream? stream = ConfigurationIcon.GetIconStream();
else if (stream == null)
LoadFromBitmap(ConfigurationIcon, stream); Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
else
LoadFromBitmap(ConfigurationIcon, stream);
}, DispatcherPriority.ApplicationIdle);
} }
} }
catch (Exception) catch (Exception)
@ -97,12 +100,12 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable
if (e.NewValue is Core.ProfileConfigurationIcon newIcon) if (e.NewValue is Core.ProfileConfigurationIcon newIcon)
newIcon.IconUpdated += ConfigurationIconOnIconUpdated; newIcon.IconUpdated += ConfigurationIconOnIconUpdated;
Dispatcher.UIThread.Post(Update, DispatcherPriority.ApplicationIdle); Update();
} }
private void ConfigurationIconOnIconUpdated(object? sender, EventArgs e) private void ConfigurationIconOnIconUpdated(object? sender, EventArgs e)
{ {
Dispatcher.UIThread.Post(Update, DispatcherPriority.ApplicationIdle); Update();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -1,4 +1,5 @@
using System.Reflection; using System.Reflection;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using DryIoc; using DryIoc;
@ -15,6 +16,7 @@ public static class ContainerExtensions
/// <param name="container">The builder building the current container</param> /// <param name="container">The builder building the current container</param>
public static void RegisterSharedUI(this IContainer container) public static void RegisterSharedUI(this IContainer container)
{ {
container.Register<IRouter, Router>(Reuse.Singleton);
Assembly artemisShared = typeof(IArtemisSharedUIService).GetAssembly(); Assembly artemisShared = typeof(IArtemisSharedUIService).GetAssembly();
container.RegisterMany(new[] {artemisShared}, type => type.IsAssignableTo<IArtemisSharedUIService>(), Reuse.Singleton); container.RegisterMany(new[] {artemisShared}, type => type.IsAssignableTo<IArtemisSharedUIService>(), Reuse.Singleton);

View File

@ -0,0 +1,21 @@
using System;
namespace Artemis.UI.Shared;
public class ArtemisRoutingException : Exception
{
/// <inheritdoc />
public ArtemisRoutingException()
{
}
/// <inheritdoc />
public ArtemisRoutingException(string? message) : base(message)
{
}
/// <inheritdoc />
public ArtemisRoutingException(string? message, Exception? innerException) : base(message, innerException)
{
}
}

View File

@ -0,0 +1,24 @@
using System.Threading;
using System.Threading.Tasks;
using ReactiveUI;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// For internal use.
/// </summary>
/// <seealso cref="RoutableScreen{TScreen}"/>
/// <seealso cref="RoutableScreen{TScreen, TParam}"/>
internal interface IRoutableScreen : IActivatableViewModel
{
/// <summary>
/// Gets or sets a value indicating whether or not to reuse the child screen instance if the type has not changed.
/// </summary>
/// <remarks>Defaults to <see langword="true"/>.</remarks>
bool RecycleScreen { get; }
object? InternalScreen { get; }
void InternalChangeScreen(object? screen);
Task InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken);
Task InternalOnClosing(NavigationArguments args);
}

View File

@ -0,0 +1,79 @@
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents a view model to which routing can take place and which in turn can host another view model.
/// </summary>
/// <typeparam name="TScreen">The type of view model the screen can host.</typeparam>
public abstract class RoutableScreen<TScreen> : ActivatableViewModelBase, IRoutableScreen where TScreen : class
{
private TScreen? _screen;
private bool _recycleScreen = true;
/// <summary>
/// Gets the currently active child screen.
/// </summary>
public TScreen? Screen
{
get => _screen;
private set => RaiseAndSetIfChanged(ref _screen, value);
}
/// <inheritdoc />
public bool RecycleScreen
{
get => _recycleScreen;
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
}
/// <summary>
/// Called before navigating to this screen.
/// </summary>
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
public virtual Task BeforeNavigating(NavigationArguments args)
{
return Task.CompletedTask;
}
/// <summary>
/// Called while navigating to this screen.
/// </summary>
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
public virtual Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
/// <summary>
/// Called before navigating away from this screen.
/// </summary>
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
public virtual Task OnClosing(NavigationArguments args)
{
return Task.CompletedTask;
}
#region Overrides of RoutableScreen
object? IRoutableScreen.InternalScreen => Screen;
void IRoutableScreen.InternalChangeScreen(object? screen)
{
Screen = screen as TScreen;
}
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
await OnNavigating(args, cancellationToken);
}
async Task IRoutableScreen.InternalOnClosing(NavigationArguments args)
{
await OnClosing(args);
}
#endregion
}

View File

@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents a view model to which routing with parameters can take place and which in turn can host another view
/// model.
/// </summary>
/// <typeparam name="TScreen">The type of view model the screen can host.</typeparam>
/// <typeparam name="TParam">The type of parameters the screen expects. It must have a parameterless constructor.</typeparam>
public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase, IRoutableScreen where TScreen : class where TParam : new()
{
private bool _recycleScreen = true;
private TScreen? _screen;
/// <summary>
/// Gets the currently active child screen.
/// </summary>
public TScreen? Screen
{
get => _screen;
private set => RaiseAndSetIfChanged(ref _screen, value);
}
/// <summary>
/// Called before navigating to this screen.
/// </summary>
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
public virtual Task BeforeNavigating(NavigationArguments args)
{
return Task.CompletedTask;
}
/// <summary>
/// Called while navigating to this screen.
/// </summary>
/// <param name="parameters">An object containing the parameters of the navigation action.</param>
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
/// <param name="cancellationToken">
/// A cancellation token that can be used by other objects or threads to receive notice of
/// cancellation.
/// </param>
public virtual Task OnNavigating(TParam parameters, NavigationArguments args, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
/// <summary>
/// Called before navigating away from this screen.
/// </summary>
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
public virtual Task OnClosing(NavigationArguments args)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public bool RecycleScreen
{
get => _recycleScreen;
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
}
#region Overrides of RoutableScreen
object? IRoutableScreen.InternalScreen => Screen;
void IRoutableScreen.InternalChangeScreen(object? screen)
{
Screen = screen as TScreen;
}
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}.");
TParam parameters = activator(args.SegmentParameters);
await OnNavigating(parameters, args, cancellationToken);
}
async Task IRoutableScreen.InternalOnClosing(NavigationArguments args)
{
await OnClosing(args);
}
#endregion
#region Parameter generation
// ReSharper disable once StaticMemberInGenericType - That's intentional, each kind of TParam should have its own property count
private static int _parameterPropertyCount;
private static Func<object[], TParam>? _parameterActivator;
private static Func<object[], TParam> GetParameterActivator()
{
if (_parameterActivator != null)
return _parameterActivator;
// Generates a lambda that creates a new instance of TParam
// - Each property of TParam with a public setter must be set using the source object[]
// - Use the index of each property as the index on the source array
// - Cast the object of the source array to the correct type for that property
Type parameterType = typeof(TParam);
ParameterExpression sourceExpression = Expression.Parameter(typeof(object[]), "source");
ParameterExpression parameterExpression = Expression.Parameter(parameterType, "parameters");
List<BinaryExpression> propertyAssignments = parameterType.GetProperties()
.Where(p => p.CanWrite)
.Select((property, index) =>
{
UnaryExpression sourceValueExpression = Expression.Convert(
Expression.ArrayIndex(sourceExpression, Expression.Constant(index)),
property.PropertyType
);
BinaryExpression propertyAssignment = Expression.Assign(
Expression.Property(parameterExpression, property),
sourceValueExpression
);
return propertyAssignment;
})
.ToList();
Expression<Func<object[], TParam>> lambda = Expression.Lambda<Func<object[], TParam>>(
Expression.Block(
new[] {parameterExpression},
Expression.Assign(parameterExpression, Expression.New(parameterType)),
Expression.Block(propertyAssignments),
parameterExpression
),
sourceExpression
);
_parameterActivator = lambda.Compile();
_parameterPropertyCount = propertyAssignments.Count;
return _parameterActivator;
}
#endregion
}

View File

@ -0,0 +1,18 @@
using System;
namespace Artemis.UI.Shared.Routing.ParameterParsers;
internal class GuidParameterParser : IRouteParameterParser
{
/// <inheritdoc />
public bool IsMatch(RouteSegment segment, string source)
{
return Guid.TryParse(source, out _);
}
/// <inheritdoc />
public object GetValue(RouteSegment segment, string source)
{
return Guid.Parse(source);
}
}

View File

@ -0,0 +1,7 @@
namespace Artemis.UI.Shared.Routing.ParameterParsers;
public interface IRouteParameterParser
{
bool IsMatch(RouteSegment segment, string source);
object GetValue(RouteSegment segment, string source);
}

View File

@ -0,0 +1,16 @@
namespace Artemis.UI.Shared.Routing.ParameterParsers;
internal class StringParameterParser : IRouteParameterParser
{
/// <inheritdoc />
public bool IsMatch(RouteSegment segment, string source)
{
return true;
}
/// <inheritdoc />
public object GetValue(RouteSegment segment, string source)
{
return source;
}
}

View File

@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Linq;
namespace Artemis.UI.Shared.Routing;
public class Route
{
public Route(string path)
{
Path = path;
Segments = path.Split('/').Select(s => new RouteSegment(s)).ToList();
}
public string Path { get; }
public List<RouteSegment> Segments { get; }
/// <inheritdoc />
public override string ToString()
{
return Path;
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Routing;
public class RouteRegistration<TViewModel> : IRouterRegistration where TViewModel : ViewModelBase
{
public RouteRegistration(string path)
{
Route = new Route(path);
}
/// <inheritdoc />
public override string ToString()
{
return $"{nameof(Route)}: {Route}, {nameof(ViewModel)}: {ViewModel}";
}
public Route Route { get; }
/// <inheritdoc />
public Type ViewModel => typeof(TViewModel);
/// <inheritdoc />
public List<IRouterRegistration> Children { get; set; } = new();
}
public interface IRouterRegistration
{
Route Route { get; }
Type ViewModel { get; }
List<IRouterRegistration> Children { get; set; }
}

View File

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DryIoc;
namespace Artemis.UI.Shared.Routing;
internal class RouteResolution
{
private RouteResolution(string path)
{
Path = path;
}
public string Path { get; }
public bool Success { get; private set; }
public Type? ViewModel { get; private set; }
public object[]? Parameters { get; private set; }
public RouteResolution? Child { get; private set; }
public static RouteResolution Resolve(IRouterRegistration registration, string path)
{
List<string> segments = path.Split('/').ToList();
if (registration.Route.Segments.Count > segments.Count)
return AsFailure(path);
// Ensure self is a match
List<object> parameters = new();
int currentSegment = 0;
foreach (RouteSegment routeSegment in registration.Route.Segments)
{
string segment = segments[currentSegment];
if (!routeSegment.IsMatch(segment))
return AsFailure(path);
object? parameter = routeSegment.GetParameter(segment);
if (parameter != null)
parameters.Add(parameter);
currentSegment++;
}
if (currentSegment == segments.Count)
return AsSuccess(registration.ViewModel, path, parameters.ToArray());
// If segments remain, a child should match it
string childPath = string.Join('/', segments.Skip(currentSegment));
foreach (IRouterRegistration routerRegistration in registration.Children)
{
RouteResolution result = Resolve(routerRegistration, childPath);
if (result.Success)
return AsSuccess(registration.ViewModel, path, parameters.ToArray(), result);
}
return AsFailure(path);
}
public static RouteResolution AsFailure(string path)
{
return new RouteResolution(path);
}
public static RouteResolution AsSuccess(Type viewModel, string path, object[] parameters, RouteResolution? child = null)
{
if (child != null && !child.Success)
throw new ArtemisRoutingException("Cannot create a success route resolution with a failed child");
return new RouteResolution(path)
{
Success = true,
ViewModel = viewModel,
Parameters = parameters,
Child = child
};
}
public object GetViewModel(IContainer container)
{
if (ViewModel == null)
throw new ArtemisRoutingException("Cannot get a view model of a non-success route resolution");
object? viewModel = container.Resolve(ViewModel);
if (viewModel == null)
throw new ArtemisRoutingException($"Could not resolve view model of type {ViewModel}");
return viewModel;
}
public T GetViewModel<T>(IContainer container)
{
if (ViewModel == null)
throw new ArtemisRoutingException("Cannot get a view model of a non-success route resolution");
object? viewModel = container.Resolve(ViewModel);
if (viewModel == null)
throw new ArtemisRoutingException($"Could not resolve view model of type {ViewModel}");
if (viewModel is not T typedViewModel)
throw new ArtemisRoutingException($"View model of type {ViewModel} is does mot implement {typeof(T)}.");
return typedViewModel;
}
public object[] GetAllParameters()
{
List<object> result = new();
if (Parameters != null)
result.AddRange(Parameters);
object[]? childParameters = Child?.GetAllParameters();
if (childParameters != null)
result.AddRange(childParameters);
return result.ToArray();
}
}

View File

@ -0,0 +1,60 @@
using System;
using System.Text.RegularExpressions;
using Artemis.UI.Shared.Routing.ParameterParsers;
namespace Artemis.UI.Shared.Routing;
public partial class RouteSegment
{
private readonly IRouteParameterParser? _parameterParser;
public RouteSegment(string segment)
{
Segment = segment;
Match match = ParameterRegex().Match(segment);
if (match.Success)
{
Parameter = match.Groups[1].Value;
ParameterType = match.Groups[2].Value;
_parameterParser = GetParameterParser(ParameterType);
}
}
public string Segment { get; }
public string? Parameter { get; }
public string? ParameterType { get; }
public bool IsMatch(string value)
{
if (_parameterParser == null)
return Segment.Equals(value, StringComparison.InvariantCultureIgnoreCase);
return _parameterParser.IsMatch(this, value);
}
public object? GetParameter(string value)
{
if (_parameterParser == null)
return null;
return _parameterParser.GetValue(this, value);
}
/// <inheritdoc />
public override string ToString()
{
return $"{Segment} (param: {Parameter ?? "none"}, type: {ParameterType ?? "N/A"})";
}
private IRouteParameterParser GetParameterParser(string parameterType)
{
if (parameterType == "guid")
return new GuidParameterParser();
// Default to a string parser which just returns the segment as is
return new StringParameterParser();
}
[GeneratedRegex(@"\{(\w+):(\w+)\}")]
private static partial Regex ParameterRegex();
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents a router that can be used to navigate to different screens.
/// </summary>
public interface IRouter
{
/// <summary>
/// Gets an observable containing the current path.
/// </summary>
IObservable<string?> CurrentPath { get; }
/// <summary>
/// Gets a list of router registrations, you can use this to register new routes.
/// </summary>
List<IRouterRegistration> Routes { get; }
/// <summary>
/// Asynchronously navigates to the provided path.
/// </summary>
/// <remarks>Navigating cancels any currently processing navigations.</remarks>
/// <param name="path">The path to navigate to.</param>
/// <param name="options">Optional navigation options used to control navigation behaviour.</param>
/// <returns>A task representing the operation</returns>
Task Navigate(string path, RouterNavigationOptions? options = null);
/// <summary>
/// Asynchronously navigates back to the previous active route.
/// </summary>
/// <returns>A task containing a boolean value which indicates whether there was a previous path to go back to.</returns>
Task<bool> GoBack();
/// <summary>
/// Asynchronously navigates forward to the previous active route.
/// </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>
/// Clears the navigation history.
/// </summary>
void ClearHistory();
/// <summary>
/// Sets the root screen from which navigation takes place.
/// </summary>
/// <param name="root">The root screen to set.</param>
/// <typeparam name="TScreen">The type of the root screen. It must be a class.</typeparam>
void SetRoot<TScreen>(RoutableScreen<TScreen> root) where TScreen : class;
/// <summary>
/// Sets the root screen from which navigation takes place.
/// </summary>
/// <param name="root">The root screen to set.</param>
/// <typeparam name="TScreen">The type of the root screen. It must be a class.</typeparam>
/// <typeparam name="TParam">The type of the parameters for the root screen. It must have a parameterless constructor.</typeparam>
void SetRoot<TScreen, TParam>(RoutableScreen<TScreen, TParam> root) where TScreen : class where TParam : new();
}

View File

@ -0,0 +1,121 @@
using System;
using System.Reactive.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using DryIoc;
using ReactiveUI;
using Serilog;
namespace Artemis.UI.Shared.Routing;
internal class Navigation
{
private readonly IContainer _container;
private readonly ILogger _logger;
private readonly IRoutableScreen _root;
private readonly RouteResolution _resolution;
private readonly RouterNavigationOptions _options;
private CancellationTokenSource _cts;
public Navigation(IContainer container, ILogger logger, IRoutableScreen root, RouteResolution resolution, RouterNavigationOptions options)
{
_container = container;
_logger = logger;
_root = root;
_resolution = resolution;
_options = options;
_cts = new CancellationTokenSource();
}
public bool Cancelled => _cts.IsCancellationRequested;
public bool Completed { get; private set; }
public async Task Navigate(NavigationArguments args)
{
_logger.Information("Navigating to {Path}", _resolution.Path);
_cts = new CancellationTokenSource();
await NavigateResolution(_resolution, args, _root);
if (!Cancelled)
_logger.Information("Navigated to {Path}", _resolution.Path);
}
public void Cancel()
{
if (Cancelled || Completed)
return;
_logger.Information("Cancelled navigation to {Path}", _resolution.Path);
_cts.Cancel();
}
private async Task NavigateResolution(RouteResolution resolution, NavigationArguments args, IRoutableScreen host)
{
if (Cancelled)
return;
// Reuse the screen if its type has not changed, if a new one must be created, don't do so on the UI thread
object screen;
if (_options.RecycleScreens && host.RecycleScreen && host.InternalScreen != null && host.InternalScreen.GetType() == resolution.ViewModel)
screen = host.InternalScreen;
else
screen = await Task.Run(() => resolution.GetViewModel(_container));
// If resolution has a child, ensure the screen can host it
if (resolution.Child != null && screen is not IRoutableScreen)
throw new ArtemisRoutingException($"Route resolved with a child but view model of type {resolution.ViewModel} is does mot implement {nameof(IRoutableScreen)}.");
// Only change the screen if it wasn't reused
if (!ReferenceEquals(host.InternalScreen, screen))
host.InternalChangeScreen(screen);
if (CancelIfRequested(args, "ChangeScreen", screen))
return;
// If the screen implements some form of Navigable, activate it
args.SegmentParameters = resolution.Parameters ?? Array.Empty<object>();
if (screen is IRoutableScreen routableScreen)
{
try
{
await routableScreen.InternalOnNavigating(args, _cts.Token);
}
catch (Exception e)
{
Cancel();
_logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
}
if (CancelIfRequested(args, "OnNavigating", screen))
return;
}
if (resolution.Child != null && screen is IRoutableScreen childScreen)
await NavigateResolution(resolution.Child, args, childScreen);
Completed = true;
}
public bool PathEquals(string path, bool allowPartialMatch)
{
if (allowPartialMatch)
return _resolution.Path.StartsWith(path, StringComparison.InvariantCultureIgnoreCase);
return string.Equals(_resolution.Path, path, StringComparison.InvariantCultureIgnoreCase);
}
private bool CancelIfRequested(NavigationArguments args, string stage, object screen)
{
if (Cancelled)
return true;
if (!args.Cancelled)
return false;
_logger.Debug("Navigation to {Path} during {Stage} by {Screen}", args.Path, stage, screen.GetType().Name);
Cancel();
return true;
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents an object that contains information about the current navigation action.
/// </summary>
public class NavigationArguments
{
internal NavigationArguments(IRouter router, string path, object[] routeParameters)
{
Router = router;
Path = path;
RouteParameters = routeParameters;
SegmentParameters = Array.Empty<object>();
}
/// <summary>
/// Gets the router in which the navigation is taking place.
/// </summary>
public IRouter Router { get; }
/// <summary>
/// Gets the path of the route that is being navigated to.
/// </summary>
public string Path { get; }
/// <summary>
/// GEts an array of all parameters provided to this route.
/// </summary>
public object[] RouteParameters { get; }
/// <summary>
/// Gets an array of parameters provided to this screen's segment of the route.
/// </summary>
public object[] SegmentParameters { get; internal set; }
internal bool Cancelled { get; private set; }
/// <summary>
/// Cancels further processing of the current navigation.
/// </summary>
/// <remarks>It not necessary to cancel the navigation in order to navigate to another route, the current navigation will be cancelled by the router.</remarks>
public void Cancel()
{
Cancelled = true;
}
}

View File

@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using Artemis.Core;
using Avalonia.Threading;
using Serilog;
namespace Artemis.UI.Shared.Routing;
internal class Router : CorePropertyChanged, IRouter
{
private readonly Stack<string> _backStack = new();
private readonly BehaviorSubject<string?> _currentRouteSubject;
private readonly Stack<string> _forwardStack = new();
private readonly Func<IRoutableScreen, RouteResolution, RouterNavigationOptions, Navigation> _getNavigation;
private readonly ILogger _logger;
private Navigation? _currentNavigation;
private IRoutableScreen? _root;
public Router(ILogger logger, Func<IRoutableScreen, RouteResolution, RouterNavigationOptions, Navigation> getNavigation)
{
_logger = logger;
_getNavigation = getNavigation;
_currentRouteSubject = new BehaviorSubject<string?>(null);
}
private RouteResolution Resolve(string path)
{
foreach (IRouterRegistration routerRegistration in Routes)
{
RouteResolution result = RouteResolution.Resolve(routerRegistration, path);
if (result.Success)
return result;
}
return RouteResolution.AsFailure(path);
}
private async Task<bool> RequestClose(object screen, NavigationArguments args)
{
if (screen is not IRoutableScreen routableScreen)
return true;
await routableScreen.InternalOnClosing(args);
if (args.Cancelled)
{
_logger.Debug("Navigation to {Path} cancelled during RequestClose by {Screen}", args.Path, screen.GetType().Name);
return false;
}
if (routableScreen.InternalScreen == null)
return true;
return await RequestClose(routableScreen.InternalScreen, args);
}
private bool PathEquals(string path, bool allowPartialMatch)
{
if (allowPartialMatch)
return _currentRouteSubject.Value != null && _currentRouteSubject.Value.StartsWith(path, StringComparison.InvariantCultureIgnoreCase);
return string.Equals(_currentRouteSubject.Value, path, StringComparison.InvariantCultureIgnoreCase);
}
/// <inheritdoc />
public IObservable<string?> CurrentPath => _currentRouteSubject;
/// <inheritdoc />
public List<IRouterRegistration> Routes { get; } = new();
/// <inheritdoc />
public async Task Navigate(string path, RouterNavigationOptions? options = null)
{
options ??= new RouterNavigationOptions();
// Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
await Dispatcher.UIThread.InvokeAsync(() => InternalNavigate(path, options));
}
private async Task InternalNavigate(string path, RouterNavigationOptions options)
{
if (_root == null)
throw new ArtemisRoutingException("Cannot navigate without a root having been set");
if (PathEquals(path, options.IgnoreOnPartialMatch) || (_currentNavigation != null && _currentNavigation.PathEquals(path, options.IgnoreOnPartialMatch)))
return;
string? previousPath = _currentRouteSubject.Value;
RouteResolution resolution = Resolve(path);
if (!resolution.Success)
{
_logger.Warning("Failed to resolve path {Path}", path);
return;
}
NavigationArguments args = new(this, resolution.Path, resolution.GetAllParameters());
if (!await RequestClose(_root, args))
return;
Navigation navigation = _getNavigation(_root, resolution, options);
_currentNavigation?.Cancel();
_currentNavigation = navigation;
// Execute the navigation
await navigation.Navigate(args);
// If it was cancelled before completion, don't add it to history or update the current path
if (navigation.Cancelled)
return;
if (options.AddToHistory && previousPath != null)
{
_backStack.Push(previousPath);
_forwardStack.Clear();
}
_currentRouteSubject.OnNext(path);
}
/// <inheritdoc />
public async Task<bool> GoBack()
{
if (!_backStack.TryPop(out string? path))
return false;
string? previousPath = _currentRouteSubject.Value;
await Navigate(path, new RouterNavigationOptions {AddToHistory = false});
if (previousPath != null)
_forwardStack.Push(previousPath);
return true;
}
/// <inheritdoc />
public async Task<bool> GoForward()
{
if (!_forwardStack.TryPop(out string? path))
return false;
string? previousPath = _currentRouteSubject.Value;
await Navigate(path, new RouterNavigationOptions {AddToHistory = false});
if (previousPath != null)
_backStack.Push(previousPath);
return true;
}
/// <inheritdoc />
public void ClearHistory()
{
_backStack.Clear();
_forwardStack.Clear();
}
/// <inheritdoc />
public void SetRoot<TScreen>(RoutableScreen<TScreen> root) where TScreen : class
{
_root = root;
}
/// <inheritdoc />
public void SetRoot<TScreen, TParam>(RoutableScreen<TScreen, TParam> root) where TScreen : class where TParam : new()
{
_root = root;
}
}

View File

@ -0,0 +1,23 @@
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents navigation options used to control navigation behaviour.
/// </summary>
public class RouterNavigationOptions
{
/// <summary>
/// Gets or sets a boolean indicating whether or not to add the navigation to the history.
/// </summary>
public bool AddToHistory { get; set; } = true;
/// <summary>
/// Gets or sets a boolean indicating whether or not to recycle already active screens.
/// </summary>
public bool RecycleScreens { get; set; } = true;
/// <summary>
/// Gets or sets a boolean indicating whether route changes should be ignored if they are a partial match.
/// </summary>
/// <example>If set to true, a route change from <c>page/subpage1/subpage2</c> to <c>page/subpage1</c> will be ignored.</example>
public bool IgnoreOnPartialMatch { get; set; } = false;
}

View File

@ -153,6 +153,7 @@ public class NotificationBuilder
public class NotificationButtonBuilder public class NotificationButtonBuilder
{ {
private Action? _action; private Action? _action;
private Func<Task>? _asyncAction;
private ICommand? _command; private ICommand? _command;
private object? _commandParameter; private object? _commandParameter;
private string _text = "Text"; private string _text = "Text";
@ -179,6 +180,18 @@ public class NotificationButtonBuilder
_action = action; _action = action;
return this; return this;
} }
/// <summary>
/// Changes action that is called when the button is clicked.
/// </summary>
/// <param name="action">The action to call when the button is clicked.</param>
/// <returns>The builder that can be used to further build the button.</returns>
public NotificationButtonBuilder WithAction(Func<Task> action)
{
_command = null;
_asyncAction = action;
return this;
}
/// <summary> /// <summary>
/// Changes command that is called when the button is clicked. /// Changes command that is called when the button is clicked.
@ -210,6 +223,8 @@ public class NotificationButtonBuilder
if (_action != null) if (_action != null)
button.Command = ReactiveCommand.Create(() => _action()); button.Command = ReactiveCommand.Create(() => _action());
else if (_asyncAction != null)
button.Command = ReactiveCommand.CreateFromTask(() => _asyncAction());
else if (_command != null) else if (_command != null)
{ {
button.Command = _command; button.Command = _command;

View File

@ -1,5 +1,4 @@
using System; using System;
using ReactiveUI;
namespace Artemis.UI.Shared.Services.MainWindow; namespace Artemis.UI.Shared.Services.MainWindow;
@ -12,12 +11,7 @@ public interface IMainWindowService : IArtemisSharedUIService
/// Gets a boolean indicating whether the main window is currently open /// Gets a boolean indicating whether the main window is currently open
/// </summary> /// </summary>
bool IsMainWindowOpen { get; } bool IsMainWindowOpen { get; }
/// <summary>
/// Gets or sets the host screen contained in the main window
/// </summary>
IScreen? HostScreen { get; set; }
/// <summary> /// <summary>
/// Sets up the main window provider that controls the state of the main window /// Sets up the main window provider that controls the state of the main window
/// </summary> /// </summary>

View File

@ -9,10 +9,7 @@ internal class MainWindowService : IMainWindowService
/// <inheritdoc /> /// <inheritdoc />
public bool IsMainWindowOpen { get; private set; } public bool IsMainWindowOpen { get; private set; }
/// <inheritdoc />
public IScreen? HostScreen { get; set; }
protected virtual void OnMainWindowOpened() protected virtual void OnMainWindowOpened()
{ {
MainWindowOpened?.Invoke(this, EventArgs.Empty); MainWindowOpened?.Invoke(this, EventArgs.Empty);
@ -20,6 +17,7 @@ internal class MainWindowService : IMainWindowService
protected virtual void OnMainWindowClosed() protected virtual void OnMainWindowClosed()
{ {
UI.ClearCache();
MainWindowClosed?.Invoke(this, EventArgs.Empty); MainWindowClosed?.Invoke(this, EventArgs.Empty);
} }

View File

@ -65,7 +65,7 @@ public interface IProfileEditorService : IArtemisSharedUIService
/// Changes the selected profile by its <see cref="Core.ProfileConfiguration" />. /// Changes the selected profile by its <see cref="Core.ProfileConfiguration" />.
/// </summary> /// </summary>
/// <param name="profileConfiguration">The profile configuration of the profile to select.</param> /// <param name="profileConfiguration">The profile configuration of the profile to select.</param>
void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration); Task ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration);
/// <summary> /// <summary>
/// Changes the selected profile element. /// Changes the selected profile element.

View File

@ -10,6 +10,7 @@ using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Shared.Services.MainWindow; using Artemis.UI.Shared.Services.MainWindow;
using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Threading;
using DynamicData; using DynamicData;
using Serilog; using Serilog;
@ -155,14 +156,13 @@ internal class ProfileEditorService : IProfileEditorService
public IObservable<ILayerProperty?> LayerProperty { get; } public IObservable<ILayerProperty?> LayerProperty { get; }
public IObservable<ProfileEditorHistory?> History { get; } public IObservable<ProfileEditorHistory?> History { get; }
public IObservable<bool> SuspendedEditing { get; } public IObservable<bool> SuspendedEditing { get; }
public IObservable<bool> SuspendedKeybindings { get; }
public IObservable<TimeSpan> Time { get; } public IObservable<TimeSpan> Time { get; }
public IObservable<bool> Playing { get; } public IObservable<bool> Playing { get; }
public IObservable<int> PixelsPerSecond { get; } public IObservable<int> PixelsPerSecond { get; }
public IObservable<ProfileEditorFocusMode> FocusMode { get; } public IObservable<ProfileEditorFocusMode> FocusMode { get; }
public ReadOnlyObservableCollection<ILayerPropertyKeyframe> SelectedKeyframes { get; } public ReadOnlyObservableCollection<ILayerPropertyKeyframe> SelectedKeyframes { get; }
public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration) public async Task ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
{ {
if (ReferenceEquals(_profileConfigurationSubject.Value, profileConfiguration)) if (ReferenceEquals(_profileConfigurationSubject.Value, profileConfiguration))
return; return;
@ -177,7 +177,7 @@ internal class ProfileEditorService : IProfileEditorService
_profileConfigurationSubject.Value.Profile.LastSelectedProfileElement = _profileElementSubject.Value; _profileConfigurationSubject.Value.Profile.LastSelectedProfileElement = _profileElementSubject.Value;
} }
SaveProfile(); await SaveProfileAsync();
// No need to deactivate the profile, if needed it will be deactivated next update // No need to deactivate the profile, if needed it will be deactivated next update
if (_profileConfigurationSubject.Value != null) if (_profileConfigurationSubject.Value != null)
@ -192,11 +192,13 @@ internal class ProfileEditorService : IProfileEditorService
// The new profile may need activation // The new profile may need activation
if (profileConfiguration != null) if (profileConfiguration != null)
{ {
profileConfiguration.IsBeingEdited = true; await Task.Run(() =>
_moduleService.SetActivationOverride(profileConfiguration.Module); {
_profileService.ActivateProfile(profileConfiguration); profileConfiguration.IsBeingEdited = true;
_profileService.RenderForEditor = true; _moduleService.SetActivationOverride(profileConfiguration.Module);
_profileService.ActivateProfile(profileConfiguration);
_profileService.RenderForEditor = true;
});
if (profileConfiguration.Profile?.LastSelectedProfileElement is RenderProfileElement renderProfileElement) if (profileConfiguration.Profile?.LastSelectedProfileElement is RenderProfileElement renderProfileElement)
ChangeCurrentProfileElement(renderProfileElement); ChangeCurrentProfileElement(renderProfileElement);
} }

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Reactive.Concurrency;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
@ -16,6 +18,13 @@ public static class UI
{ {
KeyBindingsEnabled = InputElement.GotFocusEvent.Raised.Select(e => e.Item2.Source is not TextBox).StartWith(true); KeyBindingsEnabled = InputElement.GotFocusEvent.Raised.Select(e => e.Item2.Source is not TextBox).StartWith(true);
} }
public static EventLoopScheduler BackgroundScheduler = new EventLoopScheduler(ts => new Thread(ts));
internal static void ClearCache()
{
DeviceVisualizer.BitmapCache.Clear();
}
/// <summary> /// <summary>
/// Gets the current IoC locator. /// Gets the current IoC locator.

View File

@ -17,10 +17,10 @@
<TrayIcon Icon="avares://Artemis.UI/Assets/Images/Logo/application.ico" ToolTipText="Artemis" Command="{CompiledBinding OpenScreen}" CommandParameter="Home"> <TrayIcon Icon="avares://Artemis.UI/Assets/Images/Logo/application.ico" ToolTipText="Artemis" Command="{CompiledBinding OpenScreen}" CommandParameter="Home">
<TrayIcon.Menu> <TrayIcon.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="Home" Command="{CompiledBinding OpenScreen}" CommandParameter="Home" /> <NativeMenuItem Header="Home" Command="{CompiledBinding OpenScreen}" CommandParameter="home" />
<!-- <NativeMenuItem Header="Workshop" Command="{CompiledBinding OpenScreen}" CommandParameter="Workshop" /> --> <!-- <NativeMenuItem Header="Workshop" Command="{CompiledBinding OpenScreen}" CommandParameter="workshop" /> -->
<NativeMenuItem Header="Surface Editor" Command="{CompiledBinding OpenScreen}" CommandParameter="Surface Editor" /> <NativeMenuItem Header="Surface Editor" Command="{CompiledBinding OpenScreen}" CommandParameter="surface-editor" />
<NativeMenuItem Header="Settings" Command="{CompiledBinding OpenScreen}" CommandParameter="Settings" /> <NativeMenuItem Header="Settings" Command="{CompiledBinding OpenScreen}" CommandParameter="settings/releases" />
<NativeMenuItemSeparator /> <NativeMenuItemSeparator />
<NativeMenuItem Header="Debugger" Command="{CompiledBinding OpenDebugger}" /> <NativeMenuItem Header="Debugger" Command="{CompiledBinding OpenDebugger}" />
<NativeMenuItem Header="Exit" Command="{CompiledBinding Exit}" /> <NativeMenuItem Header="Exit" Command="{CompiledBinding Exit}" />

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Windows.UI.Notifications; using Windows.UI.Notifications;
using Artemis.UI.Screens.Settings; using Artemis.UI.Screens.Settings;
using Artemis.UI.Services.Updating; using Artemis.UI.Services.Updating;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services.MainWindow; using Artemis.UI.Shared.Services.MainWindow;
using Avalonia.Threading; using Avalonia.Threading;
using Microsoft.Toolkit.Uwp.Notifications; using Microsoft.Toolkit.Uwp.Notifications;
@ -17,19 +18,16 @@ namespace Artemis.UI.Windows.Providers;
public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
{ {
private readonly Func<Guid, ReleaseInstaller> _getReleaseInstaller; private readonly Func<Guid, ReleaseInstaller> _getReleaseInstaller;
private readonly Func<IScreen, SettingsViewModel> _getSettingsViewModel;
private readonly IMainWindowService _mainWindowService; private readonly IMainWindowService _mainWindowService;
private readonly IUpdateService _updateService; private readonly IUpdateService _updateService;
private readonly IRouter _router;
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService, public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService, IUpdateService updateService, IRouter router, Func<Guid, ReleaseInstaller> getReleaseInstaller)
IUpdateService updateService,
Func<IScreen, SettingsViewModel> getSettingsViewModel,
Func<Guid, ReleaseInstaller> getReleaseInstaller)
{ {
_mainWindowService = mainWindowService; _mainWindowService = mainWindowService;
_updateService = updateService; _updateService = updateService;
_getSettingsViewModel = getSettingsViewModel; _router = router;
_getReleaseInstaller = getReleaseInstaller; _getReleaseInstaller = getReleaseInstaller;
ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated; ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated;
} }
@ -57,25 +55,15 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
.Show(); .Show();
} }
private void ViewRelease(string releaseVersion) private void ViewRelease(Guid? releaseId)
{ {
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Invoke(async () =>
{ {
_mainWindowService.OpenMainWindow(); _mainWindowService.OpenMainWindow();
if (_mainWindowService.HostScreen == null) if (releaseId != null)
return; await _router.Navigate($"settings/releases/{releaseId}");
else
// TODO: When proper routing has been implemented, use that here await _router.Navigate($"settings/releases");
// Create a settings VM to navigate to
SettingsViewModel settingsViewModel = _getSettingsViewModel(_mainWindowService.HostScreen);
// Get the release tab
ReleasesTabViewModel releaseTabViewModel = (ReleasesTabViewModel) settingsViewModel.SettingTabs.First(t => t is ReleasesTabViewModel);
// Navigate to the settings VM
_mainWindowService.HostScreen.Router.Navigate.Execute(settingsViewModel);
// Navigate to the release tab
releaseTabViewModel.PreselectVersion = releaseVersion;
settingsViewModel.SelectedTab = releaseTabViewModel;
}); });
} }
@ -173,7 +161,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
if (action == "install") if (action == "install")
await InstallRelease(releaseId, releaseVersion); await InstallRelease(releaseId, releaseVersion);
else if (action == "view-changes") else if (action == "view-changes")
ViewRelease(releaseVersion); ViewRelease(releaseId);
else if (action == "cancel") else if (action == "cancel")
_cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Cancel();
else if (action == "restart-for-update") else if (action == "restart-for-update")

View File

@ -37,6 +37,7 @@
<PackageReference Include="RGB.NET.Layout" Version="2.0.0-prerelease.83" /> <PackageReference Include="RGB.NET.Layout" Version="2.0.0-prerelease.83" />
<PackageReference Include="SkiaSharp" Version="2.88.3" /> <PackageReference Include="SkiaSharp" Version="2.88.3" />
<PackageReference Include="Splat.DryIoc" Version="14.6.8" /> <PackageReference Include="Splat.DryIoc" Version="14.6.8" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Data.Converters;
namespace Artemis.UI.Converters;
public class SubstringConverter : IValueConverter
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (parameter == null || !int.TryParse((string) parameter, out int intParameter))
return value;
return value?.ToString()?.Substring(0, intParameter);
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value;
}
}

View File

@ -5,6 +5,7 @@ using Artemis.Core;
using Artemis.Core.LayerBrushes; using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects; using Artemis.Core.LayerEffects;
using Artemis.Core.ScriptingProviders; using Artemis.Core.ScriptingProviders;
using Artemis.UI.Routing;
using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Device;
using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.Plugins.Features; using Artemis.UI.Screens.Plugins.Features;
@ -26,6 +27,8 @@ using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.VisualScripting; using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Screens.VisualScripting.Pins;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Updating;
using DryIoc; using DryIoc;
using ReactiveUI; using ReactiveUI;
@ -123,7 +126,6 @@ public class SettingsVmFactory : ISettingsVmFactory
public interface ISidebarVmFactory : IVmFactory public interface ISidebarVmFactory : IVmFactory
{ {
SidebarViewModel? SidebarViewModel(IScreen hostScreen);
SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory);
SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration);
} }
@ -135,12 +137,7 @@ public class SidebarVmFactory : ISidebarVmFactory
{ {
_container = container; _container = container;
} }
public SidebarViewModel? SidebarViewModel(IScreen hostScreen)
{
return _container.Resolve<SidebarViewModel>(new object[] { hostScreen });
}
public SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory) public SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory)
{ {
return _container.Resolve<SidebarCategoryViewModel>(new object[] { profileCategory }); return _container.Resolve<SidebarCategoryViewModel>(new object[] { profileCategory });
@ -483,7 +480,7 @@ public class ScriptVmFactory : IScriptVmFactory
public interface IReleaseVmFactory : IVmFactory public interface IReleaseVmFactory : IVmFactory
{ {
ReleaseViewModel ReleaseListViewModel(Guid releaseId, string version, DateTimeOffset createdAt); ReleaseViewModel ReleaseListViewModel(IGetReleases_PublishedReleases_Nodes release);
} }
public class ReleaseVmFactory : IReleaseVmFactory public class ReleaseVmFactory : IReleaseVmFactory
{ {
@ -494,8 +491,8 @@ public class ReleaseVmFactory : IReleaseVmFactory
_container = container; _container = container;
} }
public ReleaseViewModel ReleaseListViewModel(Guid releaseId, string version, DateTimeOffset createdAt) public ReleaseViewModel ReleaseListViewModel(IGetReleases_PublishedReleases_Nodes release)
{ {
return _container.Resolve<ReleaseViewModel>(new object[] { releaseId, version, createdAt }); return _container.Resolve<ReleaseViewModel>(new object[] { release });
} }
} }

View File

@ -10,7 +10,8 @@
Icon="/Assets/Images/Logo/application.ico" Icon="/Assets/Images/Logo/application.ico"
Title="Artemis 2.0" Title="Artemis 2.0"
MinWidth="600" MinWidth="600"
MinHeight="400"> MinHeight="400"
PointerReleased="InputElement_OnPointerReleased">
<windowing:AppWindow.Styles> <windowing:AppWindow.Styles>
<Styles> <Styles>
<Style Selector="Border#TitleBarContainer"> <Style Selector="Border#TitleBarContainer">

View File

@ -6,6 +6,7 @@ using Artemis.UI.Screens.Root;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using ReactiveUI; using ReactiveUI;
@ -79,4 +80,12 @@ public partial class MainWindow : ReactiveAppWindow<RootViewModel>
{ {
ViewModel?.Unfocused(); ViewModel?.Unfocused();
} }
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (e.InitialPressMouseButton == MouseButton.XButton1)
ViewModel?.GoBack();
else if (e.InitialPressMouseButton == MouseButton.XButton2)
ViewModel?.GoForward();
}
} }

View File

@ -0,0 +1,29 @@
using System;
using Avalonia.Controls;
using FluentAvalonia.UI.Controls;
namespace Artemis.UI;
public class PageFactory : INavigationPageFactory
{
private readonly ViewLocator _viewLocator;
public PageFactory()
{
_viewLocator = new ViewLocator();
}
/// <inheritdoc />
public Control? GetPage(Type srcType)
{
return null;
}
/// <inheritdoc />
public Control GetPageFromObject(object target)
{
Control control = _viewLocator.Build(target);
control.DataContext = target;
return control;
}
}

View File

@ -0,0 +1,38 @@
using System.Collections.Generic;
using Artemis.UI.Screens.Home;
using Artemis.UI.Screens.ProfileEditor;
using Artemis.UI.Screens.Settings;
using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.Workshop;
using Artemis.UI.Shared.Routing;
namespace Artemis.UI.Routing;
public static class Routes
{
public static List<IRouterRegistration> ArtemisRoutes = new()
{
new RouteRegistration<HomeViewModel>("home"),
new RouteRegistration<WorkshopViewModel>("workshop"),
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
new RouteRegistration<SettingsViewModel>("settings")
{
Children = new List<IRouterRegistration>
{
new RouteRegistration<GeneralTabViewModel>("general"),
new RouteRegistration<PluginsTabViewModel>("plugins"),
new RouteRegistration<DevicesTabViewModel>("devices"),
new RouteRegistration<ReleasesTabViewModel>("releases")
{
Children = new List<IRouterRegistration>()
{
new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")
}
},
new RouteRegistration<AboutTabViewModel>("about")
}
},
new RouteRegistration<ProfileEditorViewModel>("profile-editor/{profileConfigurationId:guid}")
};
}

View File

@ -1,19 +1,19 @@
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Screens.StartupWizard;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Avalonia.Threading; using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Home; namespace Artemis.UI.Screens.Home;
public class HomeViewModel : MainScreenViewModel public class HomeViewModel : ViewModelBase, IMainScreenViewModel
{ {
public HomeViewModel(IScreen hostScreen, ISettingsService settingsService, IWindowService windowService) : base(hostScreen, "home") public HomeViewModel(ISettingsService settingsService, IWindowService windowService)
{ {
DisplayName = "Home";
// Show the startup wizard if it hasn't been completed // Show the startup wizard if it hasn't been completed
if (!settingsService.GetSetting("UI.SetupWizardCompleted", false).Value) if (!settingsService.GetSetting("UI.SetupWizardCompleted", false).Value)
Dispatcher.UIThread.InvokeAsync(async () => await windowService.ShowDialogAsync<StartupWizardViewModel, bool>()); Dispatcher.UIThread.InvokeAsync(async () => await windowService.ShowDialogAsync<StartupWizardViewModel, bool>());
} }
public ViewModelBase? TitleBarViewModel => null;
} }

View File

@ -0,0 +1,8 @@
using Artemis.UI.Shared;
namespace Artemis.UI.Screens;
public interface IMainScreenViewModel
{
ViewModelBase? TitleBarViewModel { get; }
}

View File

@ -1,21 +0,0 @@
using Artemis.UI.Shared;
using ReactiveUI;
namespace Artemis.UI.Screens;
public abstract class MainScreenViewModel : ActivatableViewModelBase, IRoutableViewModel
{
protected MainScreenViewModel(IScreen hostScreen, string urlPathSegment)
{
HostScreen = hostScreen;
UrlPathSegment = urlPathSegment;
}
public ViewModelBase? TitleBarViewModel { get; protected set; }
/// <inheritdoc />
public string UrlPathSegment { get; }
/// <inheritdoc />
public IScreen HostScreen { get; }
}

View File

@ -11,6 +11,7 @@ using Artemis.Core.Services;
using Artemis.UI.Screens.Scripting; using Artemis.UI.Screens.Scripting;
using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -21,7 +22,7 @@ namespace Artemis.UI.Screens.ProfileEditor.MenuBar;
public class MenuBarViewModel : ActivatableViewModelBase public class MenuBarViewModel : ActivatableViewModelBase
{ {
private readonly ILogger _logger; private readonly IRouter _router;
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private readonly IProfileService _profileService; private readonly IProfileService _profileService;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
@ -36,9 +37,9 @@ public class MenuBarViewModel : ActivatableViewModelBase
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement; private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
private ObservableAsPropertyHelper<bool>? _suspendedEditing; private ObservableAsPropertyHelper<bool>? _suspendedEditing;
public MenuBarViewModel(ILogger logger, IProfileEditorService profileEditorService, IProfileService profileService, ISettingsService settingsService, IWindowService windowService) public MenuBarViewModel(IRouter router, IProfileEditorService profileEditorService, IProfileService profileService, ISettingsService settingsService, IWindowService windowService)
{ {
_logger = logger; _router = router;
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
_profileService = profileService; _profileService = profileService;
_settingsService = settingsService; _settingsService = settingsService;
@ -182,7 +183,7 @@ public class MenuBarViewModel : ActivatableViewModelBase
return; return;
if (ProfileConfiguration.IsBeingEdited) if (ProfileConfiguration.IsBeingEdited)
_profileEditorService.ChangeCurrentProfileConfiguration(null); await _router.Navigate("home");
_profileService.RemoveProfileConfiguration(ProfileConfiguration); _profileService.RemoveProfileConfiguration(ProfileConfiguration);
} }

View File

@ -17,6 +17,7 @@ using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia; using Avalonia;
using Avalonia.ReactiveUI;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree; namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
@ -71,7 +72,7 @@ public abstract class TreeItemViewModel : ActivatableViewModelBase
CreateTreeItems(); CreateTreeItems();
}); });
this.WhenAnyValue(vm => vm.IsFlyoutOpen).Subscribe(UpdateCanPaste); this.WhenAnyValue(vm => vm.IsFlyoutOpen).ObserveOn(AvaloniaScheduler.Instance).Subscribe(UpdateCanPaste);
} }
public ReactiveCommand<Unit, bool> AbsorbCommand { get; } public ReactiveCommand<Unit, bool> AbsorbCommand { get; }

View File

@ -83,6 +83,7 @@ public class PropertiesViewModel : ActivatableViewModelBase
? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x) ? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x)
: Observable.Never<EventPattern<object>>()) : Observable.Never<EventPattern<object>>())
.Switch() .Switch()
.ObserveOn(Shared.UI.BackgroundScheduler)
.Subscribe(_ => UpdatePropertyGroups()) .Subscribe(_ => UpdatePropertyGroups())
.DisposeWith(d); .DisposeWith(d);
this.WhenAnyValue(vm => vm.ProfileElement) this.WhenAnyValue(vm => vm.ProfileElement)
@ -90,10 +91,11 @@ public class PropertiesViewModel : ActivatableViewModelBase
? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x) ? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x)
: Observable.Never<EventPattern<object>>()) : Observable.Never<EventPattern<object>>())
.Switch() .Switch()
.ObserveOn(Shared.UI.BackgroundScheduler)
.Subscribe(_ => UpdatePropertyGroups()) .Subscribe(_ => UpdatePropertyGroups())
.DisposeWith(d); .DisposeWith(d);
}); });
this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdatePropertyGroups()); this.WhenAnyValue(vm => vm.ProfileElement).ObserveOn(Shared.UI.BackgroundScheduler).Subscribe(_ => UpdatePropertyGroups());
this.WhenAnyValue(vm => vm.LayerProperty).Subscribe(_ => UpdateTimelineViewModel()); this.WhenAnyValue(vm => vm.LayerProperty).Subscribe(_ => UpdateTimelineViewModel());
} }

View File

@ -83,7 +83,11 @@
</ItemsControl> </ItemsControl>
</Grid> </Grid>
</paz:ZoomBorder> </paz:ZoomBorder>
<Border CornerRadius="0 0 8 0" VerticalAlignment="Top" HorizontalAlignment="Left" Background="{DynamicResource ControlFillColorDefaultBrush}"> <Border CornerRadius="0 0 8 0"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Background="{DynamicResource ControlFillColorDefaultBrush}"
IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Margin="8"> <StackPanel Orientation="Horizontal" Margin="8">
<shared:ProfileConfigurationIcon ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}" Width="18" Height="18" Margin="0 0 5 0" /> <shared:ProfileConfigurationIcon ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}" Width="18" Height="18" Margin="0 0 5 0" />
<TextBlock Text="{CompiledBinding ProfileConfiguration.Name}" /> <TextBlock Text="{CompiledBinding ProfileConfiguration.Name}" />

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls.PanAndZoom; using Avalonia.Controls.PanAndZoom;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
@ -17,7 +18,7 @@ public partial class LayerShapeVisualizerView : ReactiveUserControl<LayerShapeVi
public LayerShapeVisualizerView() public LayerShapeVisualizerView()
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Selected).Subscribe(_ => UpdateStrokeThickness()).DisposeWith(d)); this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Selected).ObserveOn(AvaloniaScheduler.Instance).Subscribe(_ => UpdateStrokeThickness()).DisposeWith(d));
} }

View File

@ -12,14 +12,6 @@
<UserControl.Resources> <UserControl.Resources>
<converters:DoubleToGridLengthConverter x:Key="DoubleToGridLengthConverter" /> <converters:DoubleToGridLengthConverter x:Key="DoubleToGridLengthConverter" />
</UserControl.Resources> </UserControl.Resources>
<UserControl.Styles>
<Style Selector="Border.suspended-editing">
<Setter Property="Margin" Value="-10" />
<Setter Property="Background" Value="{DynamicResource SmokeFillColorDefault}" />
<Setter Property="IsVisible" Value="{CompiledBinding SuspendedEditing}" />
<Setter Property="CornerRadius" Value="{DynamicResource CardCornerRadius}" />
</Style>
</UserControl.Styles>
<UserControl.KeyBindings> <UserControl.KeyBindings>
<KeyBinding Command="{CompiledBinding History.Undo}" Gesture="Ctrl+Z" /> <KeyBinding Command="{CompiledBinding History.Undo}" Gesture="Ctrl+Z" />
<KeyBinding Command="{CompiledBinding History.Redo}" Gesture="Ctrl+Y" /> <KeyBinding Command="{CompiledBinding History.Redo}" Gesture="Ctrl+Y" />
@ -30,6 +22,12 @@
<KeyBinding Command="{Binding TitleBarViewModel.MenuBarViewModel.CycleFocusMode}" Gesture="F" /> <KeyBinding Command="{Binding TitleBarViewModel.MenuBarViewModel.CycleFocusMode}" Gesture="F" />
</UserControl.KeyBindings> </UserControl.KeyBindings>
<UserControl.Styles> <UserControl.Styles>
<Style Selector="Border.suspended-editing">
<Setter Property="Margin" Value="-10" />
<Setter Property="Background" Value="{DynamicResource SmokeFillColorDefault}" />
<Setter Property="IsVisible" Value="{CompiledBinding SuspendedEditing}" />
<Setter Property="CornerRadius" Value="{DynamicResource CardCornerRadius}" />
</Style>
<Style Selector="GridSplitter.editor-grid-splitter-vertical"> <Style Selector="GridSplitter.editor-grid-splitter-vertical">
<Setter Property="MinWidth" Value="4" /> <Setter Property="MinWidth" Value="4" />
<Setter Property="Margin" Value="1 1 1 5" /> <Setter Property="Margin" Value="1 1 1 5" />
@ -70,7 +68,7 @@
<Border Grid.Row="0" Classes="card" Padding="0" Margin="4 0 4 4" ClipToBounds="True"> <Border Grid.Row="0" Classes="card" Padding="0" Margin="4 0 4 4" ClipToBounds="True">
<Grid ColumnDefinitions="Auto,*"> <Grid ColumnDefinitions="Auto,*">
<Border Grid.Column="0"> <Border Grid.Column="0">
<ItemsControl ItemsSource="{CompiledBinding Tools}"> <ItemsControl ItemsSource="{CompiledBinding Tools}" IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate DataType="shared:IToolViewModel"> <DataTemplate DataType="shared:IToolViewModel">
<ToggleButton Classes="icon-button editor-sidebar-button" <ToggleButton Classes="icon-button editor-sidebar-button"
@ -83,14 +81,15 @@
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
</Border> </Border>
<ContentControl Grid.Column="1" Content="{CompiledBinding VisualEditorViewModel}" /> <ContentControl Grid.Column="1" Content="{CompiledBinding VisualEditorViewModel}" Classes="fade-in"
IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}" />
</Grid> </Grid>
</Border> </Border>
<GridSplitter Grid.Row="1" Classes="editor-grid-splitter-horizontal" /> <GridSplitter Grid.Row="1" Classes="editor-grid-splitter-horizontal" />
<Border Grid.Row="2" Classes="card card-condensed" Margin="4" Padding="0" ClipToBounds="True"> <Border Grid.Row="2" Classes="card card-condensed" Margin="4" Padding="0" ClipToBounds="True">
<Panel> <Panel IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}">
<ContentControl Content="{CompiledBinding PropertiesViewModel}" /> <ContentControl Content="{CompiledBinding PropertiesViewModel}" />
<Border Classes="suspended-editing"> <Border Classes="suspended-editing">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="16"> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="16">
@ -109,9 +108,9 @@
</Panel> </Panel>
</Border> </Border>
</Grid> </Grid>
<GridSplitter Grid.Row="0" Grid.Column="1" Classes="editor-grid-splitter-vertical" /> <GridSplitter Grid.Row="0" Grid.Column="1" Classes="editor-grid-splitter-vertical" />
<Grid Grid.Row="0" Grid.Column="2"> <Grid Grid.Row="0" Grid.Column="2">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <RowDefinition Height="*" />
@ -119,22 +118,25 @@
<RowDefinition Height="{CompiledBinding ConditionsHeight.Value, Mode=TwoWay, Converter={StaticResource DoubleToGridLengthConverter}}" /> <RowDefinition Height="{CompiledBinding ConditionsHeight.Value, Mode=TwoWay, Converter={StaticResource DoubleToGridLengthConverter}}" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border Grid.Row="0" Classes="card card-condensed" Margin="4 0 4 4"> <Border Grid.Row="0" Classes="card card-condensed" Margin="4 0 4 4">
<Panel> <Panel IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}">
<ContentControl Content="{CompiledBinding ProfileTreeViewModel}" /> <ContentControl Content="{CompiledBinding ProfileTreeViewModel}" />
<Border Classes="suspended-editing" /> <Border Classes="suspended-editing" />
</Panel> </Panel>
</Border> </Border>
<GridSplitter Grid.Row="1" Classes="editor-grid-splitter-horizontal" /> <GridSplitter Grid.Row="1" Classes="editor-grid-splitter-horizontal" />
<Border Grid.Row="2" Classes="card card-condensed" Margin="4"> <Border Grid.Row="2" Classes="card card-condensed" Margin="4">
<Panel> <Panel IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}">
<ContentControl Content="{CompiledBinding DisplayConditionScriptViewModel}" /> <ContentControl Content="{CompiledBinding DisplayConditionScriptViewModel}" />
<Border Classes="suspended-editing" /> <Border Classes="suspended-editing" />
</Panel> </Panel>
</Border> </Border>
</Grid> </Grid>
<ContentControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Content="{CompiledBinding StatusBarViewModel}" /> <Panel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Height="23">
<ContentControl Content="{CompiledBinding StatusBarViewModel}" IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}"/>
</Panel>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -4,6 +4,8 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Screens.ProfileEditor.DisplayCondition; using Artemis.UI.Screens.ProfileEditor.DisplayCondition;
@ -11,6 +13,8 @@ using Artemis.UI.Screens.ProfileEditor.ProfileTree;
using Artemis.UI.Screens.ProfileEditor.Properties; using Artemis.UI.Screens.ProfileEditor.Properties;
using Artemis.UI.Screens.ProfileEditor.StatusBar; using Artemis.UI.Screens.ProfileEditor.StatusBar;
using Artemis.UI.Screens.ProfileEditor.VisualEditor; using Artemis.UI.Screens.ProfileEditor.VisualEditor;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services.MainWindow; using Artemis.UI.Shared.Services.MainWindow;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Threading; using Avalonia.Threading;
@ -20,22 +24,18 @@ using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor; namespace Artemis.UI.Screens.ProfileEditor;
public class ProfileEditorViewModel : MainScreenViewModel public class ProfileEditorViewModel : RoutableScreen<object, ProfileEditorViewModelParameters>, IMainScreenViewModel
{ {
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private readonly IProfileService _profileService;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly SourceList<IToolViewModel> _tools; private readonly SourceList<IToolViewModel> _tools;
private DisplayConditionScriptViewModel? _displayConditionScriptViewModel;
private ObservableAsPropertyHelper<ProfileEditorHistory?>? _history; private ObservableAsPropertyHelper<ProfileEditorHistory?>? _history;
private ObservableAsPropertyHelper<ProfileConfiguration?>? _profileConfiguration; private ProfileConfiguration? _profileConfiguration;
private ProfileTreeViewModel? _profileTreeViewModel;
private PropertiesViewModel? _propertiesViewModel;
private StatusBarViewModel? _statusBarViewModel;
private ObservableAsPropertyHelper<bool>? _suspendedEditing; private ObservableAsPropertyHelper<bool>? _suspendedEditing;
private VisualEditorViewModel? _visualEditorViewModel;
/// <inheritdoc /> /// <inheritdoc />
public ProfileEditorViewModel(IScreen hostScreen, public ProfileEditorViewModel(IProfileService profileService,
IProfileEditorService profileEditorService, IProfileEditorService profileEditorService,
ISettingsService settingsService, ISettingsService settingsService,
VisualEditorViewModel visualEditorViewModel, VisualEditorViewModel visualEditorViewModel,
@ -46,8 +46,8 @@ public class ProfileEditorViewModel : MainScreenViewModel
StatusBarViewModel statusBarViewModel, StatusBarViewModel statusBarViewModel,
IEnumerable<IToolViewModel> toolViewModels, IEnumerable<IToolViewModel> toolViewModels,
IMainWindowService mainWindowService) IMainWindowService mainWindowService)
: base(hostScreen, "profile-editor")
{ {
_profileService = profileService;
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
_settingsService = settingsService; _settingsService = settingsService;
@ -62,9 +62,14 @@ public class ProfileEditorViewModel : MainScreenViewModel
Tools = tools; Tools = tools;
visualEditorViewModel.SetTools(_tools); visualEditorViewModel.SetTools(_tools);
StatusBarViewModel = statusBarViewModel;
VisualEditorViewModel = visualEditorViewModel;
ProfileTreeViewModel = profileTreeViewModel;
PropertiesViewModel = propertiesViewModel;
DisplayConditionScriptViewModel = displayConditionScriptViewModel;
this.WhenActivated(d => this.WhenActivated(d =>
{ {
_profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d);
_history = profileEditorService.History.ToProperty(this, vm => vm.History).DisposeWith(d); _history = profileEditorService.History.ToProperty(this, vm => vm.History).DisposeWith(d);
_suspendedEditing = profileEditorService.SuspendedEditing.ToProperty(this, vm => vm.SuspendedEditing).DisposeWith(d); _suspendedEditing = profileEditorService.SuspendedEditing.ToProperty(this, vm => vm.SuspendedEditing).DisposeWith(d);
@ -78,13 +83,6 @@ public class ProfileEditorViewModel : MainScreenViewModel
foreach (IToolViewModel toolViewModel in _tools.Items) foreach (IToolViewModel toolViewModel in _tools.Items)
toolViewModel.Dispose(); toolViewModel.Dispose();
}).DisposeWith(d); }).DisposeWith(d);
// Slow and steady wins the race (and doesn't lock up the entire UI)
Dispatcher.UIThread.Post(() => StatusBarViewModel = statusBarViewModel, DispatcherPriority.Loaded);
Dispatcher.UIThread.Post(() => VisualEditorViewModel = visualEditorViewModel, DispatcherPriority.Loaded);
Dispatcher.UIThread.Post(() => ProfileTreeViewModel = profileTreeViewModel, DispatcherPriority.Loaded);
Dispatcher.UIThread.Post(() => PropertiesViewModel = propertiesViewModel, DispatcherPriority.Loaded);
Dispatcher.UIThread.Post(() => DisplayConditionScriptViewModel = displayConditionScriptViewModel, DispatcherPriority.Loaded);
}); });
TitleBarViewModel = profileEditorTitleBarViewModel; TitleBarViewModel = profileEditorTitleBarViewModel;
@ -92,38 +90,19 @@ public class ProfileEditorViewModel : MainScreenViewModel
ToggleAutoSuspend = ReactiveCommand.Create(ExecuteToggleAutoSuspend); ToggleAutoSuspend = ReactiveCommand.Create(ExecuteToggleAutoSuspend);
} }
public VisualEditorViewModel? VisualEditorViewModel public ProfileConfiguration? ProfileConfiguration
{ {
get => _visualEditorViewModel; get => _profileConfiguration;
set => RaiseAndSetIfChanged(ref _visualEditorViewModel, value); set => RaiseAndSetIfChanged(ref _profileConfiguration, value);
} }
public ProfileTreeViewModel? ProfileTreeViewModel public VisualEditorViewModel? VisualEditorViewModel { get; }
{ public ProfileTreeViewModel? ProfileTreeViewModel { get; }
get => _profileTreeViewModel; public PropertiesViewModel? PropertiesViewModel { get; }
set => RaiseAndSetIfChanged(ref _profileTreeViewModel, value); public DisplayConditionScriptViewModel? DisplayConditionScriptViewModel { get; }
} public StatusBarViewModel? StatusBarViewModel { get; }
public PropertiesViewModel? PropertiesViewModel
{
get => _propertiesViewModel;
set => RaiseAndSetIfChanged(ref _propertiesViewModel, value);
}
public DisplayConditionScriptViewModel? DisplayConditionScriptViewModel
{
get => _displayConditionScriptViewModel;
set => RaiseAndSetIfChanged(ref _displayConditionScriptViewModel, value);
}
public StatusBarViewModel? StatusBarViewModel
{
get => _statusBarViewModel;
set => RaiseAndSetIfChanged(ref _statusBarViewModel, value);
}
public ReadOnlyObservableCollection<IToolViewModel> Tools { get; } public ReadOnlyObservableCollection<IToolViewModel> Tools { get; }
public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value;
public ProfileEditorHistory? History => _history?.Value; public ProfileEditorHistory? History => _history?.Value;
public bool SuspendedEditing => _suspendedEditing?.Value ?? false; public bool SuspendedEditing => _suspendedEditing?.Value ?? false;
public PluginSetting<double> TreeWidth => _settingsService.GetSetting("ProfileEditor.TreeWidth", 350.0); public PluginSetting<double> TreeWidth => _settingsService.GetSetting("ProfileEditor.TreeWidth", 350.0);
@ -132,11 +111,6 @@ public class ProfileEditorViewModel : MainScreenViewModel
public ReactiveCommand<Unit, Unit> ToggleSuspend { get; } public ReactiveCommand<Unit, Unit> ToggleSuspend { get; }
public ReactiveCommand<Unit, Unit> ToggleAutoSuspend { get; } public ReactiveCommand<Unit, Unit> ToggleAutoSuspend { get; }
public void OpenUrl(string url)
{
Utilities.OpenUrl(url);
}
private void ExecuteToggleSuspend() private void ExecuteToggleSuspend()
{ {
_profileEditorService.ChangeSuspendedEditing(!SuspendedEditing); _profileEditorService.ChangeSuspendedEditing(!SuspendedEditing);
@ -177,4 +151,41 @@ public class ProfileEditorViewModel : MainScreenViewModel
if (_settingsService.GetSetting("ProfileEditor.AutoSuspend", true).Value) if (_settingsService.GetSetting("ProfileEditor.AutoSuspend", true).Value)
_profileEditorService.ChangeSuspendedEditing(true); _profileEditorService.ChangeSuspendedEditing(true);
} }
public ViewModelBase? TitleBarViewModel { get; }
#region Overrides of RoutableScreen<object,ProfileEditorViewModelParameters>
/// <inheritdoc />
public override async Task OnNavigating(ProfileEditorViewModelParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
ProfileConfiguration? profileConfiguration = _profileService.ProfileConfigurations.FirstOrDefault(c => c.ProfileId == parameters.ProfileId);
// If the profile doesn't exist, navigate home for lack of some kind of 404 :p
if (profileConfiguration == null)
{
await args.Router.Navigate("home");
return;
}
await _profileEditorService.ChangeCurrentProfileConfiguration(profileConfiguration);
ProfileConfiguration = profileConfiguration;
}
/// <inheritdoc />
public override async Task OnClosing(NavigationArguments args)
{
if (!args.Path.StartsWith("profile-editor"))
{
ProfileConfiguration = null;
await _profileEditorService.ChangeCurrentProfileConfiguration(null);
}
}
#endregion
}
public class ProfileEditorViewModelParameters
{
public Guid ProfileId { get; set; }
} }

View File

@ -2,14 +2,15 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:reactiveUi="http://reactiveui.net"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:vm="clr-namespace:Artemis.UI.Screens.Root;assembly=Artemis.UI" xmlns:vm="clr-namespace:Artemis.UI.Screens.Root;assembly=Artemis.UI"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
x:DataType="vm:RootViewModel" x:DataType="vm:RootViewModel"
x:Class="Artemis.UI.Screens.Root.RootView"> x:Class="Artemis.UI.Screens.Root.RootView">
<reactiveUi:RoutedViewHost Router="{CompiledBinding Router}"> <controls:Frame Name="RootFrame" IsNavigationStackEnabled="False" CacheSize="0">
<reactiveUi:RoutedViewHost.PageTransition> <controls:Frame.NavigationPageFactory>
<CompositePageTransition></CompositePageTransition> <ui:PageFactory/>
</reactiveUi:RoutedViewHost.PageTransition> </controls:Frame.NavigationPageFactory>
</reactiveUi:RoutedViewHost> </controls:Frame>
</UserControl> </UserControl>

View File

@ -1,5 +1,8 @@
using Avalonia.Markup.Xaml; using System;
using System.Reactive.Disposables;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Root; namespace Artemis.UI.Screens.Root;
@ -8,6 +11,18 @@ public partial class RootView : ReactiveUserControl<RootViewModel>
public RootView() public RootView()
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen).Subscribe(Navigate).DisposeWith(d));
} }
private void Navigate(IMainScreenViewModel viewModel)
{
try
{
Dispatcher.UIThread.Invoke(() => RootFrame.NavigateFromObject(viewModel));
}
catch (Exception)
{
// ignored
}
}
} }

View File

@ -1,69 +1,67 @@
using System; using System;
using System.Linq;
using System.Reactive; using System.Reactive;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Models; using Artemis.UI.Models;
using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Services.Interfaces; using Artemis.UI.Services.Interfaces;
using Artemis.UI.Services.Updating; using Artemis.UI.Services.Updating;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.MainWindow; using Artemis.UI.Shared.Services.MainWindow;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.Root; namespace Artemis.UI.Screens.Root;
public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvider public class RootViewModel : RoutableScreen<IMainScreenViewModel>, IMainWindowProvider
{ {
private readonly IRouter _router;
private readonly ICoreService _coreService; private readonly ICoreService _coreService;
private readonly IDebugService _debugService; private readonly IDebugService _debugService;
private readonly DefaultTitleBarViewModel _defaultTitleBarViewModel; private readonly DefaultTitleBarViewModel _defaultTitleBarViewModel;
private readonly IClassicDesktopStyleApplicationLifetime _lifeTime; private readonly IClassicDesktopStyleApplicationLifetime _lifeTime;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly ISidebarVmFactory _sidebarVmFactory;
private readonly IUpdateService _updateService; private readonly IUpdateService _updateService;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private SidebarViewModel? _sidebarViewModel;
private ViewModelBase? _titleBarViewModel; private ViewModelBase? _titleBarViewModel;
public RootViewModel(ICoreService coreService, public RootViewModel(IRouter router,
ICoreService coreService,
ISettingsService settingsService, ISettingsService settingsService,
IRegistrationService registrationService, IRegistrationService registrationService,
IWindowService windowService, IWindowService windowService,
IMainWindowService mainWindowService, IMainWindowService mainWindowService,
IDebugService debugService, IDebugService debugService,
IUpdateService updateService, IUpdateService updateService,
DefaultTitleBarViewModel defaultTitleBarViewModel, SidebarViewModel sidebarViewModel,
ISidebarVmFactory sidebarVmFactory) DefaultTitleBarViewModel defaultTitleBarViewModel)
{ {
Router = new RoutingState();
WindowSizeSetting = settingsService.GetSetting<WindowSize?>("WindowSize"); WindowSizeSetting = settingsService.GetSetting<WindowSize?>("WindowSize");
SidebarViewModel = sidebarViewModel;
_router = router;
_coreService = coreService; _coreService = coreService;
_settingsService = settingsService; _settingsService = settingsService;
_windowService = windowService; _windowService = windowService;
_debugService = debugService; _debugService = debugService;
_updateService = updateService; _updateService = updateService;
_defaultTitleBarViewModel = defaultTitleBarViewModel; _defaultTitleBarViewModel = defaultTitleBarViewModel;
_sidebarVmFactory = sidebarVmFactory;
_lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!; _lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!;
router.SetRoot(this);
mainWindowService.ConfigureMainWindowProvider(this); mainWindowService.ConfigureMainWindowProvider(this);
mainWindowService.HostScreen = this;
DisplayAccordingToSettings(); DisplayAccordingToSettings();
OpenScreen = ReactiveCommand.Create<string>(ExecuteOpenScreen); OpenScreen = ReactiveCommand.Create<string>(ExecuteOpenScreen);
OpenDebugger = ReactiveCommand.CreateFromTask(ExecuteOpenDebugger); OpenDebugger = ReactiveCommand.CreateFromTask(ExecuteOpenDebugger);
Exit = ReactiveCommand.CreateFromTask(ExecuteExit); Exit = ReactiveCommand.CreateFromTask(ExecuteExit);
this.WhenAnyValue(vm => vm.Screen).Subscribe(UpdateTitleBarViewModel);
Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel);
Task.Run(() => Task.Run(() =>
{ {
if (_updateService.Initialize()) if (_updateService.Initialize())
@ -76,26 +74,33 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
}); });
} }
public SidebarViewModel SidebarViewModel { get; }
public ReactiveCommand<string, Unit> OpenScreen { get; } public ReactiveCommand<string, Unit> OpenScreen { get; }
public ReactiveCommand<Unit, Unit> OpenDebugger { get; } public ReactiveCommand<Unit, Unit> OpenDebugger { get; }
public ReactiveCommand<Unit, Unit> Exit { get; } public ReactiveCommand<Unit, Unit> Exit { get; }
public SidebarViewModel? SidebarViewModel
{
get => _sidebarViewModel;
set => RaiseAndSetIfChanged(ref _sidebarViewModel, value);
}
public ViewModelBase? TitleBarViewModel public ViewModelBase? TitleBarViewModel
{ {
get => _titleBarViewModel; get => _titleBarViewModel;
set => RaiseAndSetIfChanged(ref _titleBarViewModel, value); set => RaiseAndSetIfChanged(ref _titleBarViewModel, value);
} }
private void UpdateTitleBarViewModel(IRoutableViewModel? viewModel) public void GoBack()
{ {
if (viewModel is MainScreenViewModel mainScreenViewModel && mainScreenViewModel.TitleBarViewModel != null) _router.GoBack();
TitleBarViewModel = mainScreenViewModel.TitleBarViewModel; }
public void GoForward()
{
_router.GoForward();
}
public static PluginSetting<WindowSize?>? WindowSizeSetting { get; private set; }
private void UpdateTitleBarViewModel(IMainScreenViewModel? viewModel)
{
if (viewModel?.TitleBarViewModel != null)
TitleBarViewModel = viewModel.TitleBarViewModel;
else else
TitleBarViewModel = _defaultTitleBarViewModel; TitleBarViewModel = _defaultTitleBarViewModel;
} }
@ -104,8 +109,6 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
{ {
WindowSizeSetting?.Save(); WindowSizeSetting?.Save();
_lifeTime.MainWindow = null; _lifeTime.MainWindow = null;
SidebarViewModel = null;
Router.NavigateAndReset.Execute(new EmptyViewModel(this, "blank")).Subscribe();
OnMainWindowClosed(); OnMainWindowClosed();
} }
@ -127,14 +130,9 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
_windowService.ShowWindow<SplashViewModel>(); _windowService.ShowWindow<SplashViewModel>();
} }
/// <inheritdoc />
public RoutingState Router { get; }
public static PluginSetting<WindowSize?>? WindowSizeSetting { get; private set; }
#region Tray commands #region Tray commands
private void ExecuteOpenScreen(string displayName) private void ExecuteOpenScreen(string path)
{ {
// The window will open on the UI thread at some point, respond to that to select the chosen screen // The window will open on the UI thread at some point, respond to that to select the chosen screen
MainWindowOpened += OnEventHandler; MainWindowOpened += OnEventHandler;
@ -142,10 +140,9 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
void OnEventHandler(object? sender, EventArgs args) void OnEventHandler(object? sender, EventArgs args)
{ {
// Avoid threading issues by running this on the UI thread
if (SidebarViewModel != null)
Dispatcher.UIThread.InvokeAsync(() => SidebarViewModel.SelectedSidebarScreen = SidebarViewModel.SidebarScreens.FirstOrDefault(s => s.DisplayName == displayName));
MainWindowOpened -= OnEventHandler; MainWindowOpened -= OnEventHandler;
// Avoid threading issues by running this on the UI thread
Dispatcher.UIThread.InvokeAsync(async () => await _router.Navigate(path));
} }
} }
@ -176,7 +173,6 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
{ {
if (_lifeTime.MainWindow == null) if (_lifeTime.MainWindow == null)
{ {
SidebarViewModel = _sidebarVmFactory.SidebarViewModel(this);
_lifeTime.MainWindow = new MainWindow {DataContext = this}; _lifeTime.MainWindow = new MainWindow {DataContext = this};
_lifeTime.MainWindow.Show(); _lifeTime.MainWindow.Show();
_lifeTime.MainWindow.Closing += CurrentMainWindowOnClosing; _lifeTime.MainWindow.Closing += CurrentMainWindowOnClosing;
@ -237,12 +233,4 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
} }
#endregion #endregion
}
internal class EmptyViewModel : MainScreenViewModel
{
/// <inheritdoc />
public EmptyViewModel(IScreen hostScreen, string urlPathSegment) : base(hostScreen, urlPathSegment)
{
}
} }

View File

@ -0,0 +1,20 @@
using System;
namespace Artemis.UI.Screens.Settings;
public class SettingsTab
{
public SettingsTab(string path, string name)
{
Path = path;
Name = name;
}
public string Path { get; set; }
public string Name { get; set; }
public bool Matches(string path)
{
return path.StartsWith($"settings/{Path}", StringComparison.InvariantCultureIgnoreCase);
}
}

View File

@ -3,16 +3,25 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings" xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Settings.SettingsView" x:Class="Artemis.UI.Screens.Settings.SettingsView"
x:DataType="settings:SettingsViewModel"> x:DataType="settings:SettingsViewModel">
<Border Classes="router-container"> <Border Classes="router-container">
<TabControl Margin="12" ItemsSource="{CompiledBinding SettingTabs}" SelectedItem="{CompiledBinding SelectedTab}"> <Grid RowDefinitions="Auto,*">
<TabControl.ItemTemplate> <TabStrip Grid.Row="0" Margin="12" ItemsSource="{CompiledBinding SettingTabs}" SelectedItem="{CompiledBinding SelectedTab}">
<DataTemplate> <TabStrip.ItemTemplate>
<TextBlock Text="{CompiledBinding DisplayName}" /> <DataTemplate>
</DataTemplate> <TextBlock Text="{CompiledBinding Name}" />
</TabControl.ItemTemplate> </DataTemplate>
</TabControl> </TabStrip.ItemTemplate>
</TabStrip>
<controls:Frame Grid.Row="1" Name="TabFrame" IsNavigationStackEnabled="False" CacheSize="0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory/>
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</Grid>
</Border> </Border>
</UserControl> </UserControl>

View File

@ -1,5 +1,11 @@
using Avalonia.Markup.Xaml; using System;
using System.Reactive.Disposables;
using Artemis.UI.Shared;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading;
using FluentAvalonia.UI.Media.Animation;
using FluentAvalonia.UI.Navigation;
using ReactiveUI;
namespace Artemis.UI.Screens.Settings; namespace Artemis.UI.Screens.Settings;
@ -8,6 +14,11 @@ public partial class SettingsView : ReactiveUserControl<SettingsViewModel>
public SettingsView() public SettingsView()
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(d => { ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d); });
} }
private void Navigate(ViewModelBase viewModel)
{
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel, new FrameNavigationOptions {TransitionInfoOverride = new SlideNavigationTransitionInfo()}));
}
} }

View File

@ -1,36 +1,57 @@
using System.Collections.ObjectModel; using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.Settings; namespace Artemis.UI.Screens.Settings;
public class SettingsViewModel : MainScreenViewModel public class SettingsViewModel : RoutableScreen<ActivatableViewModelBase>, IMainScreenViewModel
{ {
private ActivatableViewModelBase _selectedTab; private readonly IRouter _router;
private SettingsTab? _selectedTab;
public SettingsViewModel(IScreen hostScreen, public SettingsViewModel(IRouter router)
GeneralTabViewModel generalTabViewModel,
PluginsTabViewModel pluginsTabViewModel,
DevicesTabViewModel devicesTabViewModel,
ReleasesTabViewModel releasesTabViewModel,
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
{ {
SettingTabs = new ObservableCollection<ActivatableViewModelBase> _router = router;
SettingTabs = new ObservableCollection<SettingsTab>
{ {
generalTabViewModel, new("general", "General"),
pluginsTabViewModel, new("plugins", "Plugins"),
devicesTabViewModel, new("devices", "Devices"),
releasesTabViewModel, new("releases", "Releases"),
aboutTabViewModel new("about", "About"),
}; };
_selectedTab = generalTabViewModel;
// Navigate on tab change
this.WhenActivated(d => this.WhenAnyValue(vm => vm.SelectedTab)
.WhereNotNull()
.Subscribe(s => _router.Navigate($"settings/{s.Path}", new RouterNavigationOptions {IgnoreOnPartialMatch = true}))
.DisposeWith(d));
} }
public ObservableCollection<ActivatableViewModelBase> SettingTabs { get; } public ObservableCollection<SettingsTab> SettingTabs { get; }
public ActivatableViewModelBase SelectedTab public SettingsTab? SelectedTab
{ {
get => _selectedTab; get => _selectedTab;
set => RaiseAndSetIfChanged(ref _selectedTab, value); set => RaiseAndSetIfChanged(ref _selectedTab, value);
} }
public ViewModelBase? TitleBarViewModel => null;
/// <inheritdoc />
public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
// Display tab change on navigate
SelectedTab = SettingTabs.FirstOrDefault(t => t.Matches(args.Path));
// Always show a tab, if there is none forward to the first
if (SelectedTab == null)
await _router.Navigate($"settings/{SettingTabs.First().Path}");
}
} }

View File

@ -32,6 +32,7 @@
</DataTemplate> </DataTemplate>
</ItemsRepeater.ItemTemplate> </ItemsRepeater.ItemTemplate>
</ItemsRepeater> </ItemsRepeater>
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -49,7 +49,7 @@ public class PluginsTabViewModel : ActivatableViewModelBase
this.WhenActivated(d => this.WhenActivated(d =>
{ {
Dispatcher.UIThread.Post(() => plugins.AddRange(_pluginManagementService.GetAllPlugins()), DispatcherPriority.Background); plugins.AddRange(_pluginManagementService.GetAllPlugins());
Observable.FromEventPattern<PluginEventArgs>(x => _pluginManagementService.PluginLoaded += x, x => _pluginManagementService.PluginLoaded -= x) Observable.FromEventPattern<PluginEventArgs>(x => _pluginManagementService.PluginLoaded += x, x => _pluginManagementService.PluginLoaded -= x)
.Subscribe(a => plugins.Add(a.EventArgs.Plugin)) .Subscribe(a => plugins.Add(a.EventArgs.Plugin))
.DisposeWith(d); .DisposeWith(d);

View File

@ -6,6 +6,7 @@
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating" xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400" mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
x:Class="Artemis.UI.Screens.Settings.ReleasesTabView" x:Class="Artemis.UI.Screens.Settings.ReleasesTabView"
x:DataType="settings:ReleasesTabViewModel"> x:DataType="settings:ReleasesTabViewModel">
@ -34,31 +35,16 @@
</controls:HyperlinkButton> </controls:HyperlinkButton>
</StackPanel> </StackPanel>
<Grid ColumnDefinitions="300,*" Margin="0 10" IsVisible="{CompiledBinding ReleaseViewModels.Count}"> <Grid ColumnDefinitions="300,*" Margin="10" IsVisible="{CompiledBinding ReleaseViewModels.Count}">
<Border Classes="card-condensed" Grid.Column="0" Margin="0 0 10 0"> <Border Classes="card-condensed" Grid.Column="0" Margin="0 0 10 0">
<ListBox ItemsSource="{CompiledBinding ReleaseViewModels}" SelectedItem="{CompiledBinding SelectedReleaseViewModel}"> <ListBox ItemsSource="{CompiledBinding ReleaseViewModels}" SelectedItem="{CompiledBinding SelectedReleaseViewModel}"/>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="updating:ReleaseViewModel">
<Panel>
<Grid Margin="4" IsVisible="{CompiledBinding ShowStatusIndicator}" RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" Text="{CompiledBinding Version}" VerticalAlignment="Center" FontWeight="SemiBold" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{CompiledBinding CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
<avalonia:MaterialIcon Classes="status-icon" Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Kind="CheckCircle" ToolTip.Tip="Current version"
IsVisible="{CompiledBinding IsCurrentVersion}" />
<avalonia:MaterialIcon Classes="status-icon" Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Kind="History" ToolTip.Tip="Previous version"
IsVisible="{CompiledBinding IsPreviousVersion}" />
</Grid>
<StackPanel Margin="4" IsVisible="{CompiledBinding !ShowStatusIndicator}">
<TextBlock Text="{CompiledBinding Version}" VerticalAlignment="Center" />
<TextBlock Text="{CompiledBinding CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
</StackPanel>
</Panel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border> </Border>
<ContentControl Grid.Column="1" Content="{CompiledBinding SelectedReleaseViewModel}" /> <controls:Frame Grid.Column="1" Name="ReleaseFrame" CacheSize="0" IsNavigationStackEnabled="False">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</Grid> </Grid>
</Panel> </Panel>
</Panel> </Panel>

View File

@ -1,5 +1,9 @@
using Avalonia.Markup.Xaml; using System;
using System.Reactive.Disposables;
using Artemis.UI.Shared;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Settings; namespace Artemis.UI.Screens.Settings;
@ -8,6 +12,22 @@ public partial class ReleasesTabView : ReactiveUserControl<ReleasesTabViewModel>
public ReleasesTabView() public ReleasesTabView()
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d));
}
private void Navigate(ViewModelBase viewModel)
{
Dispatcher.UIThread.Invoke(() =>
{
try
{
ReleaseFrame.NavigateFromObject(viewModel);
}
catch (Exception e)
{
// ignored
}
});
} }
} }

View File

@ -1,14 +1,15 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.UI.DryIoc.Factories; using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Extensions;
using Artemis.UI.Screens.Settings.Updating; using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Services.Updating; using Artemis.UI.Services.Updating;
using Artemis.UI.Shared; using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Updating; using Artemis.WebClient.Updating;
@ -21,47 +22,46 @@ using StrawberryShake;
namespace Artemis.UI.Screens.Settings; namespace Artemis.UI.Screens.Settings;
public class ReleasesTabViewModel : ActivatableViewModelBase public class ReleasesTabViewModel : RoutableScreen<ReleaseDetailsViewModel>
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IUpdateService _updateService; private readonly IUpdateService _updateService;
private readonly IUpdatingClient _updatingClient; private readonly IUpdatingClient _updatingClient;
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly IRouter _router;
private readonly SourceList<IGetReleases_PublishedReleases_Nodes> _releases; private readonly SourceList<IGetReleases_PublishedReleases_Nodes> _releases;
private IGetReleases_PublishedReleases_PageInfo? _lastPageInfo;
private bool _loading; private bool _loading;
private IGetReleases_PublishedReleases_PageInfo? _lastPageInfo;
private ReleaseViewModel? _selectedReleaseViewModel; private ReleaseViewModel? _selectedReleaseViewModel;
public ReleasesTabViewModel(ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, IReleaseVmFactory releaseVmFactory, INotificationService notificationService) public ReleasesTabViewModel(ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, IReleaseVmFactory releaseVmFactory, INotificationService notificationService,
IRouter router)
{ {
_logger = logger; _logger = logger;
_updateService = updateService; _updateService = updateService;
_updatingClient = updatingClient; _updatingClient = updatingClient;
_notificationService = notificationService; _notificationService = notificationService;
_router = router;
_releases = new SourceList<IGetReleases_PublishedReleases_Nodes>(); _releases = new SourceList<IGetReleases_PublishedReleases_Nodes>();
_releases.Connect() _releases.Connect()
.Sort(SortExpressionComparer<IGetReleases_PublishedReleases_Nodes>.Descending(p => p.CreatedAt)) .Sort(SortExpressionComparer<IGetReleases_PublishedReleases_Nodes>.Descending(p => p.CreatedAt))
.Transform(r => releaseVmFactory.ReleaseListViewModel(r.Id, r.Version, r.CreatedAt)) .Transform(releaseVmFactory.ReleaseListViewModel)
.ObserveOn(AvaloniaScheduler.Instance) .ObserveOn(AvaloniaScheduler.Instance)
.Bind(out ReadOnlyObservableCollection<ReleaseViewModel> releaseViewModels) .Bind(out ReadOnlyObservableCollection<ReleaseViewModel> releaseViewModels)
.Subscribe(); .Subscribe();
DisplayName = "Releases"; DisplayName = "Releases";
RecycleScreen = false;
ReleaseViewModels = releaseViewModels; ReleaseViewModels = releaseViewModels;
Channel = _updateService.Channel; Channel = _updateService.Channel;
this.WhenActivated(async d =>
{
await _updateService.CacheLatestRelease();
await GetMoreReleases(d.AsCancellationToken());
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(r => r.Version == PreselectVersion) ?? ReleaseViewModels.FirstOrDefault();
});
}
this.WhenAnyValue(vm => vm.SelectedReleaseViewModel).WhereNotNull().Subscribe(r => _router.Navigate($"settings/releases/{r.Release.Id}"));
this.WhenActivated(d => _updateService.CacheLatestRelease().ToObservable().Subscribe().DisposeWith(d));
}
public ReadOnlyObservableCollection<ReleaseViewModel> ReleaseViewModels { get; } public ReadOnlyObservableCollection<ReleaseViewModel> ReleaseViewModels { get; }
public string Channel { get; } public string Channel { get; }
public string? PreselectVersion { get; set; }
public ReleaseViewModel? SelectedReleaseViewModel public ReleaseViewModel? SelectedReleaseViewModel
{ {
@ -109,4 +109,22 @@ public class ReleasesTabViewModel : ActivatableViewModelBase
Loading = false; Loading = false;
} }
} }
/// <inheritdoc />
public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
if (!ReleaseViewModels.Any())
await GetMoreReleases(cancellationToken);
// If there is an ID parameter further down the path, preselect it
if (args.RouteParameters.Length > 0 && args.RouteParameters[0] is Guid releaseId)
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(vm => vm.Release.Id == releaseId);
// Otherwise forward to the last release
else
{
ReleaseViewModel? lastRelease = ReleaseViewModels.FirstOrDefault(r => r.IsCurrentVersion) ?? ReleaseViewModels.FirstOrDefault();
if (lastRelease != null)
await _router.Navigate($"settings/releases/{lastRelease.Release.Id}");
}
}
} }

View File

@ -0,0 +1,152 @@
<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:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
xmlns:avalonia1="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:converters1="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseDetailsView"
x:DataType="updating:ReleaseDetailsViewModel">
<UserControl.Resources>
<converters:BytesToStringConverter x:Key="BytesToStringConverter" />
<converters1:SubstringConverter x:Key="SubstringConverter" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="Grid.info-container">
<Setter Property="Margin" Value="10" />
</Style>
<Style Selector="avalonia1|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,*">
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
<StackPanel>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Classes="h4 no-margin">Release info</TextBlock>
<Panel Grid.Column="1" IsVisible="{CompiledBinding InstallationAvailable}">
<!-- Install progress -->
<Grid ColumnDefinitions="*,*"
RowDefinitions="*,*"
IsVisible="{CompiledBinding InstallationInProgress}">
<ProgressBar Grid.Column="0"
Grid.Row="0"
Width="300"
Value="{CompiledBinding ReleaseInstaller.Progress, FallbackValue=0}">
</ProgressBar>
<TextBlock Grid.Column="0"
Grid.Row="1"
Classes="subtitle"
TextAlignment="Right"
Text="{CompiledBinding ReleaseInstaller.Status, FallbackValue=Installing}" />
<Button Grid.Column="1" Grid.Row="0" Grid.RowSpan="2"
Classes="accent"
Margin="15 0 0 0"
Width="80"
VerticalAlignment="Center"
Command="{CompiledBinding CancelInstall}">
Cancel
</Button>
</Grid>
<Panel IsVisible="{CompiledBinding !InstallationInProgress}" HorizontalAlignment="Right">
<!-- Install button -->
<Button Classes="accent"
Width="80"
Command="{CompiledBinding Install}"
IsVisible="{CompiledBinding !InstallationFinished}">
Install
</Button>
<!-- Restart button -->
<Grid ColumnDefinitions="*,*" IsVisible="{CompiledBinding InstallationFinished}">
<TextBlock Grid.Column="0"
Grid.Row="0"
Classes="subtitle"
TextAlignment="Right"
VerticalAlignment="Center">
Ready, restart to install
</TextBlock>
<Button Grid.Column="1" Grid.Row="0"
Classes="accent"
Margin="15 0 0 0"
Width="80"
Command="{CompiledBinding Restart}"
IsVisible="{CompiledBinding InstallationFinished}">
Restart
</Button>
</Grid>
</Panel>
</Panel>
</Grid>
<Border Classes="card-separator" />
<Grid Margin="-5 -10" ColumnDefinitions="*,*,*">
<Grid Grid.Column="0" ColumnDefinitions="*,*" RowDefinitions="*,*,*" Classes="info-container" HorizontalAlignment="Left">
<avalonia1: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, StringFormat={}{0:g}, FallbackValue=Loading...}" />
</Grid>
<Grid Grid.Column="1" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Center">
<avalonia1:MaterialIcon Kind="Git" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Source</TextBlock>
<TextBlock Grid.Column="1"
Grid.Row="1"
Classes="info-body info-link"
Cursor="Hand"
PointerReleased="InputElement_OnPointerReleased"
Text="{CompiledBinding Release.Commit, Converter={StaticResource SubstringConverter}, ConverterParameter=7, FallbackValue=Loading...}" />
</Grid>
<Grid Grid.Column="2" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Right">
<avalonia1:MaterialIcon Kind="BoxOutline" 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 FileSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay, FallbackValue=Loading...}" />
</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" />
<avalonia:MarkdownScrollViewer Grid.Row="2" Markdown="{CompiledBinding Release.Changelog}" MarkdownStyleName="FluentAvalonia">
<avalonia:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml"/>
</avalonia:MarkdownScrollViewer.Styles>
</avalonia:MarkdownScrollViewer>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@ -0,0 +1,19 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings.Updating;
public partial class ReleaseDetailsView : ReactiveUserControl<ReleaseDetailsViewModel>
{
public ReleaseDetailsView()
{
InitializeComponent();
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
ViewModel?.NavigateToSource();
}
}

View File

@ -0,0 +1,195 @@
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Updating;
using ReactiveUI;
using Serilog;
using StrawberryShake;
namespace Artemis.UI.Screens.Settings.Updating;
public class ReleaseDetailsViewModel : RoutableScreen<ViewModelBase, ReleaseDetailsViewModelParameters>
{
private readonly ObservableAsPropertyHelper<long> _fileSize;
private readonly ILogger _logger;
private readonly INotificationService _notificationService;
private readonly IUpdateService _updateService;
private readonly IUpdatingClient _updatingClient;
private bool _installationAvailable;
private bool _installationFinished;
private bool _installationInProgress;
private CancellationTokenSource? _installerCts;
private bool _loading = true;
private IGetReleaseById_PublishedRelease? _release;
private ReleaseInstaller? _releaseInstaller;
public ReleaseDetailsViewModel(ILogger logger, IUpdatingClient updatingClient, INotificationService notificationService, IUpdateService updateService)
{
_logger = logger;
_updatingClient = updatingClient;
_notificationService = notificationService;
_updateService = updateService;
Platform updatePlatform;
if (OperatingSystem.IsWindows())
updatePlatform = Platform.Windows;
else if (OperatingSystem.IsLinux())
updatePlatform = Platform.Linux;
else if (OperatingSystem.IsMacOS())
updatePlatform = Platform.Osx;
else
throw new PlatformNotSupportedException("Cannot auto update on the current platform");
Install = ReactiveCommand.CreateFromTask(ExecuteInstall);
Restart = ReactiveCommand.Create(ExecuteRestart);
CancelInstall = ReactiveCommand.Create(() => _installerCts?.Cancel());
_fileSize = this.WhenAnyValue(vm => vm.Release)
.Select(release => release?.Artifacts.FirstOrDefault(a => a.Platform == updatePlatform)?.FileInfo.DownloadSize ?? 0)
.ToProperty(this, vm => vm.FileSize);
this.WhenActivated(d => Disposable.Create(_installerCts, cts => cts?.Cancel()).DisposeWith(d));
}
public ReactiveCommand<Unit, Unit> Restart { get; }
public ReactiveCommand<Unit, Unit> Install { get; }
public ReactiveCommand<Unit, Unit> CancelInstall { get; }
public long FileSize => _fileSize.Value;
public IGetReleaseById_PublishedRelease? Release
{
get => _release;
set => RaiseAndSetIfChanged(ref _release, value);
}
public ReleaseInstaller? ReleaseInstaller
{
get => _releaseInstaller;
set => RaiseAndSetIfChanged(ref _releaseInstaller, value);
}
public bool Loading
{
get => _loading;
private set => RaiseAndSetIfChanged(ref _loading, value);
}
public bool InstallationAvailable
{
get => _installationAvailable;
set => RaiseAndSetIfChanged(ref _installationAvailable, value);
}
public bool InstallationInProgress
{
get => _installationInProgress;
set => RaiseAndSetIfChanged(ref _installationInProgress, value);
}
public bool InstallationFinished
{
get => _installationFinished;
set => RaiseAndSetIfChanged(ref _installationFinished, value);
}
public void NavigateToSource()
{
if (Release != null)
Utilities.OpenUrl($"https://github.com/Artemis-RGB/Artemis/commit/{Release.Commit}");
}
/// <inheritdoc />
public override async Task OnNavigating(ReleaseDetailsViewModelParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
// There's no point in running anything but the latest version of the current channel.
// Perhaps later that won't be true anymore, then we could consider allowing to install
// older versions with compatible database versions.
InstallationAvailable = _updateService.CachedLatestRelease?.Id == parameters.ReleaseId;
await RetrieveDetails(parameters.ReleaseId, cancellationToken);
ReleaseInstaller = _updateService.GetReleaseInstaller(parameters.ReleaseId);
}
private void ExecuteRestart()
{
_updateService.RestartForUpdate(false);
}
private async Task ExecuteInstall(CancellationToken cancellationToken)
{
if (ReleaseInstaller == null)
return;
_installerCts = new CancellationTokenSource();
try
{
InstallationInProgress = true;
await ReleaseInstaller.InstallAsync(_installerCts.Token);
InstallationFinished = true;
}
catch (Exception e)
{
if (_installerCts.IsCancellationRequested)
return;
_logger.Warning(e, "Failed to install update through UI");
_notificationService.CreateNotification()
.WithTitle("Failed to install update")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Warning)
.Show();
}
finally
{
InstallationInProgress = false;
}
}
private async Task RetrieveDetails(Guid releaseId, CancellationToken cancellationToken)
{
try
{
Loading = true;
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(releaseId, cancellationToken);
IGetReleaseById_PublishedRelease? release = result.Data?.PublishedRelease;
if (release == null)
return;
Release = release;
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
_logger.Warning(e, "Failed to retrieve release details");
_notificationService.CreateNotification()
.WithTitle("Failed to retrieve details")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Warning)
.Show();
}
finally
{
Loading = false;
}
}
}
public class ReleaseDetailsViewModelParameters
{
public Guid ReleaseId { get; set; }
}

View File

@ -3,163 +3,22 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating" xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:avalonia1="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseView" x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseView"
x:DataType="updating:ReleaseViewModel"> x:DataType="updating:ReleaseViewModel">
<UserControl.Resources> <Panel>
<converters:BytesToStringConverter x:Key="BytesToStringConverter" /> <Grid Margin="4" IsVisible="{CompiledBinding ShowStatusIndicator}" RowDefinitions="*,*" ColumnDefinitions="*,Auto">
</UserControl.Resources> <TextBlock Grid.Row="0" Grid.Column="0" Text="{CompiledBinding Release.Version}" VerticalAlignment="Center" FontWeight="SemiBold" />
<UserControl.Styles> <TextBlock Grid.Row="1" Grid.Column="0" Text="{CompiledBinding Release.CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
<Style Selector=":is(Control).fade-in"> <avalonia:MaterialIcon Classes="status-icon" Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Kind="CheckCircle" ToolTip.Tip="Current version"
<Setter Property="Opacity" Value="0"></Setter> IsVisible="{CompiledBinding IsCurrentVersion}" />
</Style> <avalonia:MaterialIcon Classes="status-icon" Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Kind="History" ToolTip.Tip="Previous version"
<Style Selector=":is(Control).fade-in[IsVisible=True]"> IsVisible="{CompiledBinding IsPreviousVersion}" />
<Style.Animations> </Grid>
<Animation Duration="0:00:00.250" FillMode="Forward" Easing="CubicEaseInOut"> <StackPanel Margin="4" IsVisible="{CompiledBinding !ShowStatusIndicator}">
<KeyFrame Cue="0%"> <TextBlock Text="{CompiledBinding Release.Version}" VerticalAlignment="Center" />
<Setter Property="Opacity" Value="0.0" /> <TextBlock Text="{CompiledBinding Release.CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
</KeyFrame> </StackPanel>
<KeyFrame Cue="100%"> </Panel>
<Setter Property="Opacity" Value="1.0" /> </UserControl>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Grid.info-container">
<Setter Property="Margin" Value="10" />
</Style>
<Style Selector="avalonia1|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,*" IsVisible="{CompiledBinding Commit, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" Classes="fade-in">
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
<StackPanel>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Classes="h4 no-margin">Release info</TextBlock>
<Panel Grid.Column="1" IsVisible="{CompiledBinding InstallationAvailable}">
<!-- Install progress -->
<Grid ColumnDefinitions="*,*"
RowDefinitions="*,*"
IsVisible="{CompiledBinding InstallationInProgress}">
<ProgressBar Grid.Column="0"
Grid.Row="0"
Width="300"
Value="{CompiledBinding ReleaseInstaller.Progress, FallbackValue=0}">
</ProgressBar>
<TextBlock Grid.Column="0"
Grid.Row="1"
Classes="subtitle"
TextAlignment="Right"
Text="{CompiledBinding ReleaseInstaller.Status, FallbackValue=Installing}" />
<Button Grid.Column="1" Grid.Row="0" Grid.RowSpan="2"
Classes="accent"
Margin="15 0 0 0"
Width="80"
VerticalAlignment="Center"
Command="{CompiledBinding CancelInstall}">
Cancel
</Button>
</Grid>
<Panel IsVisible="{CompiledBinding !InstallationInProgress}" HorizontalAlignment="Right">
<!-- Install button -->
<Button Classes="accent"
Width="80"
Command="{CompiledBinding Install}"
IsVisible="{CompiledBinding !InstallationFinished}">
Install
</Button>
<!-- Restart button -->
<Grid ColumnDefinitions="*,*" IsVisible="{CompiledBinding InstallationFinished}">
<TextBlock Grid.Column="0"
Grid.Row="0"
Classes="subtitle"
TextAlignment="Right"
VerticalAlignment="Center">
Ready, restart to install
</TextBlock>
<Button Grid.Column="1" Grid.Row="0"
Classes="accent"
Margin="15 0 0 0"
Width="80"
Command="{CompiledBinding Restart}"
IsVisible="{CompiledBinding InstallationFinished}">
Restart
</Button>
</Grid>
</Panel>
</Panel>
</Grid>
<Border Classes="card-separator" />
<Grid Margin="-5 -10" ColumnDefinitions="*,*,*">
<Grid Grid.Column="0" ColumnDefinitions="*,*" RowDefinitions="*,*,*" Classes="info-container" HorizontalAlignment="Left">
<avalonia1: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 CreatedAt, StringFormat={}{0:g}, FallbackValue=Loading...}" />
</Grid>
<Grid Grid.Column="1" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Center">
<avalonia1:MaterialIcon Kind="Git" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Source</TextBlock>
<TextBlock Grid.Column="1"
Grid.Row="1"
Classes="info-body info-link"
Cursor="Hand"
PointerReleased="InputElement_OnPointerReleased"
Text="{CompiledBinding ShortCommit, FallbackValue=Loading...}" />
</Grid>
<Grid Grid.Column="2" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Right">
<avalonia1:MaterialIcon Kind="BoxOutline" 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 FileSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay, FallbackValue=Loading...}" />
</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" />
<avalonia:MarkdownScrollViewer Grid.Row="2" Markdown="{CompiledBinding Changelog}" MarkdownStyleName="FluentAvalonia">
<avalonia:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml"/>
</avalonia:MarkdownScrollViewer.Styles>
</avalonia:MarkdownScrollViewer>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@ -1,19 +1,18 @@
using Avalonia.Input; using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings.Updating; namespace Artemis.UI.Screens.Settings.Updating;
public partial class ReleaseView : ReactiveUserControl<ReleaseViewModel> public partial class ReleaseView : UserControl
{ {
public ReleaseView() public ReleaseView()
{ {
InitializeComponent(); InitializeComponent();
} }
private void InitializeComponent()
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{ {
ViewModel?.NavigateToSource(); AvaloniaXamlLoader.Load(this);
} }
} }

View File

@ -1,220 +1,22 @@
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Extensions;
using Artemis.UI.Services.Updating; using Artemis.UI.Services.Updating;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Updating; using Artemis.WebClient.Updating;
using ReactiveUI;
using Serilog;
using StrawberryShake;
namespace Artemis.UI.Screens.Settings.Updating; namespace Artemis.UI.Screens.Settings.Updating;
public class ReleaseViewModel : ActivatableViewModelBase public class ReleaseViewModel : ViewModelBase
{ {
private readonly ILogger _logger;
private readonly INotificationService _notificationService;
private readonly IUpdateService _updateService; private readonly IUpdateService _updateService;
private readonly Platform _updatePlatform; public IGetReleases_PublishedReleases_Nodes Release { get; }
private readonly IUpdatingClient _updatingClient;
private CancellationTokenSource? _installerCts;
private string? _changelog;
private string? _commit;
private string? _shortCommit;
private long _fileSize;
private bool _installationAvailable;
private bool _installationFinished;
private bool _installationInProgress;
private bool _loading = true;
private bool _retrievedDetails;
public ReleaseViewModel(Guid releaseId, public ReleaseViewModel(IUpdateService updateService, IGetReleases_PublishedReleases_Nodes release)
string version,
DateTimeOffset createdAt,
ILogger logger,
IUpdatingClient updatingClient,
INotificationService notificationService,
IUpdateService updateService)
{ {
_logger = logger;
_updatingClient = updatingClient;
_notificationService = notificationService;
_updateService = updateService; _updateService = updateService;
Release = release;
if (OperatingSystem.IsWindows())
_updatePlatform = Platform.Windows;
else if (OperatingSystem.IsLinux())
_updatePlatform = Platform.Linux;
else if (OperatingSystem.IsMacOS())
_updatePlatform = Platform.Osx;
else
throw new PlatformNotSupportedException("Cannot auto update on the current platform");
ReleaseId = releaseId;
Version = version;
CreatedAt = createdAt;
ReleaseInstaller = updateService.GetReleaseInstaller(ReleaseId);
Install = ReactiveCommand.CreateFromTask(ExecuteInstall);
Restart = ReactiveCommand.Create(ExecuteRestart);
CancelInstall = ReactiveCommand.Create(() => _installerCts?.Cancel());
this.WhenActivated(d =>
{
// There's no point in running anything but the latest version of the current channel.
// Perhaps later that won't be true anymore, then we could consider allowing to install
// older versions with compatible database versions.
InstallationAvailable = _updateService.CachedLatestRelease?.Id == ReleaseId;
RetrieveDetails(d.AsCancellationToken()).ToObservable();
Disposable.Create(_installerCts, cts => cts?.Cancel()).DisposeWith(d);
});
} }
public Guid ReleaseId { get; }
private void ExecuteRestart()
{
_updateService.RestartForUpdate(false);
}
public ReactiveCommand<Unit, Unit> Restart { get; set; }
public ReactiveCommand<Unit, Unit> Install { get; }
public ReactiveCommand<Unit, Unit> CancelInstall { get; }
public string Version { get; }
public DateTimeOffset CreatedAt { get; }
public ReleaseInstaller ReleaseInstaller { get; }
public string? Changelog
{
get => _changelog;
set => RaiseAndSetIfChanged(ref _changelog, value);
}
public string? Commit
{
get => _commit;
set => RaiseAndSetIfChanged(ref _commit, value);
}
public string? ShortCommit
{
get => _shortCommit;
set => RaiseAndSetIfChanged(ref _shortCommit, value);
}
public long FileSize
{
get => _fileSize;
set => RaiseAndSetIfChanged(ref _fileSize, value);
}
public bool Loading
{
get => _loading;
private set => RaiseAndSetIfChanged(ref _loading, value);
}
public bool InstallationAvailable
{
get => _installationAvailable;
set => RaiseAndSetIfChanged(ref _installationAvailable, value);
}
public bool InstallationInProgress
{
get => _installationInProgress;
set => RaiseAndSetIfChanged(ref _installationInProgress, value);
}
public bool InstallationFinished
{
get => _installationFinished;
set => RaiseAndSetIfChanged(ref _installationFinished, value);
}
public bool IsCurrentVersion => Version == Constants.CurrentVersion;
public bool IsPreviousVersion => Version == _updateService.PreviousVersion;
public bool ShowStatusIndicator => IsCurrentVersion || IsPreviousVersion;
public void NavigateToSource() public bool IsCurrentVersion => Release.Version == Constants.CurrentVersion;
{ public bool IsPreviousVersion => Release.Version == _updateService.PreviousVersion;
Utilities.OpenUrl($"https://github.com/Artemis-RGB/Artemis/commit/{Commit}"); public bool ShowStatusIndicator => IsCurrentVersion || IsPreviousVersion;
}
private async Task ExecuteInstall(CancellationToken cancellationToken)
{
_installerCts = new CancellationTokenSource();
try
{
InstallationInProgress = true;
await ReleaseInstaller.InstallAsync(_installerCts.Token);
InstallationFinished = true;
}
catch (Exception e)
{
if (_installerCts.IsCancellationRequested)
return;
_logger.Warning(e, "Failed to install update through UI");
_notificationService.CreateNotification()
.WithTitle("Failed to install update")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Warning)
.Show();
}
finally
{
InstallationInProgress = false;
}
}
private async Task RetrieveDetails(CancellationToken cancellationToken)
{
if (_retrievedDetails)
return;
try
{
Loading = true;
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(ReleaseId, cancellationToken);
IGetReleaseById_PublishedRelease? release = result.Data?.PublishedRelease;
if (release == null)
return;
Changelog = release.Changelog;
Commit = release.Commit;
ShortCommit = release.Commit.Substring(0, 7);
FileSize = release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform)?.FileInfo.DownloadSize ?? 0;
_retrievedDetails = true;
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
_logger.Warning(e, "Failed to retrieve release details");
_notificationService.CreateNotification()
.WithTitle("Failed to retrieve details")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Warning)
.Show();
}
finally
{
Loading = false;
}
}
} }

View File

@ -149,7 +149,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase<ProfileConf
return; return;
if (_profileConfiguration.IsBeingEdited) if (_profileConfiguration.IsBeingEdited)
_profileEditorService.ChangeCurrentProfileConfiguration(null); await _profileEditorService.ChangeCurrentProfileConfiguration(null);
_profileService.RemoveProfileConfiguration(_profileConfiguration); _profileService.RemoveProfileConfiguration(_profileConfiguration);
Close(_profileConfiguration); Close(_profileConfiguration);
} }

View File

@ -11,9 +11,9 @@ using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories; using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.ProfileEditor;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -24,23 +24,19 @@ namespace Artemis.UI.Screens.Sidebar;
public class SidebarCategoryViewModel : ActivatableViewModelBase public class SidebarCategoryViewModel : ActivatableViewModelBase
{ {
private readonly IProfileService _profileService; private readonly IProfileService _profileService;
private readonly ISidebarVmFactory _vmFactory;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly IProfileEditorService _profileEditorService; private readonly ISidebarVmFactory _vmFactory;
private readonly IRouter _router;
private ObservableAsPropertyHelper<bool>? _isCollapsed; private ObservableAsPropertyHelper<bool>? _isCollapsed;
private ObservableAsPropertyHelper<bool>? _isSuspended; private ObservableAsPropertyHelper<bool>? _isSuspended;
private SidebarProfileConfigurationViewModel? _selectedProfileConfiguration; private SidebarProfileConfigurationViewModel? _selectedProfileConfiguration;
public SidebarCategoryViewModel(ProfileCategory profileCategory, public SidebarCategoryViewModel(ProfileCategory profileCategory, IProfileService profileService, IWindowService windowService, ISidebarVmFactory vmFactory, IRouter router)
IProfileService profileService,
IWindowService windowService,
IProfileEditorService profileEditorService,
ISidebarVmFactory vmFactory)
{ {
_profileService = profileService; _profileService = profileService;
_windowService = windowService; _windowService = windowService;
_profileEditorService = profileEditorService;
_vmFactory = vmFactory; _vmFactory = vmFactory;
_router = router;
ProfileCategory = profileCategory; ProfileCategory = profileCategory;
SourceList<ProfileConfiguration> profileConfigurations = new(); SourceList<ProfileConfiguration> profileConfigurations = new();
@ -66,6 +62,14 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase
this.WhenActivated(d => this.WhenActivated(d =>
{ {
// Navigate on selection change
this.WhenAnyValue(vm => vm.SelectedProfileConfiguration)
.WhereNotNull()
.Subscribe(s => _router.Navigate($"profile-editor/{s.ProfileConfiguration.ProfileId}", new RouterNavigationOptions {IgnoreOnPartialMatch = true, RecycleScreens = false}))
.DisposeWith(d);
_router.CurrentPath.WhereNotNull().Subscribe(r => SelectedProfileConfiguration = ProfileConfigurations.FirstOrDefault(c => c.Matches(r))).DisposeWith(d);
// Update the list of profiles whenever the category fires events // Update the list of profiles whenever the category fires events
Observable.FromEventPattern<ProfileConfigurationEventArgs>(x => profileCategory.ProfileConfigurationAdded += x, x => profileCategory.ProfileConfigurationAdded -= x) Observable.FromEventPattern<ProfileConfigurationEventArgs>(x => profileCategory.ProfileConfigurationAdded += x, x => profileCategory.ProfileConfigurationAdded -= x)
.Subscribe(e => profileConfigurations.Add(e.EventArgs.ProfileConfiguration)) .Subscribe(e => profileConfigurations.Add(e.EventArgs.ProfileConfiguration))
@ -73,34 +77,9 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase
Observable.FromEventPattern<ProfileConfigurationEventArgs>(x => profileCategory.ProfileConfigurationRemoved += x, x => profileCategory.ProfileConfigurationRemoved -= x) Observable.FromEventPattern<ProfileConfigurationEventArgs>(x => profileCategory.ProfileConfigurationRemoved += x, x => profileCategory.ProfileConfigurationRemoved -= x)
.Subscribe(e => profileConfigurations.RemoveMany(profileConfigurations.Items.Where(c => c == e.EventArgs.ProfileConfiguration))) .Subscribe(e => profileConfigurations.RemoveMany(profileConfigurations.Items.Where(c => c == e.EventArgs.ProfileConfiguration)))
.DisposeWith(d); .DisposeWith(d);
profileEditorService.ProfileConfiguration
.Subscribe(p => SelectedProfileConfiguration = ProfileConfigurations.FirstOrDefault(c => ReferenceEquals(c.ProfileConfiguration, p)))
.DisposeWith(d);
_isCollapsed = ProfileCategory.WhenAnyValue(vm => vm.IsCollapsed).ToProperty(this, vm => vm.IsCollapsed).DisposeWith(d); _isCollapsed = ProfileCategory.WhenAnyValue(vm => vm.IsCollapsed).ToProperty(this, vm => vm.IsCollapsed).DisposeWith(d);
_isSuspended = ProfileCategory.WhenAnyValue(vm => vm.IsSuspended).ToProperty(this, vm => vm.IsSuspended).DisposeWith(d); _isSuspended = ProfileCategory.WhenAnyValue(vm => vm.IsSuspended).ToProperty(this, vm => vm.IsSuspended).DisposeWith(d);
// Change the current profile configuration when a new one is selected
this.WhenAnyValue(vm => vm.SelectedProfileConfiguration)
.WhereNotNull()
.Subscribe(s =>
{
try
{
profileEditorService.ChangeCurrentProfileConfiguration(s.ProfileConfiguration);
}
catch (Exception e)
{
if (s.ProfileConfiguration.BrokenState != null && s.ProfileConfiguration.BrokenStateException != null)
_windowService.ShowExceptionDialog(s.ProfileConfiguration.BrokenState, s.ProfileConfiguration.BrokenStateException);
else
_windowService.ShowExceptionDialog(e.Message, e);
profileEditorService.ChangeCurrentProfileConfiguration(null);
SelectedProfileConfiguration = null;
}
});
}); });
profileConfigurations.Edit(updater => profileConfigurations.Edit(updater =>
@ -158,7 +137,7 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase
if (await _windowService.ShowConfirmContentDialog($"Delete {ProfileCategory.Name}", "Do you want to delete this category and all its profiles?")) if (await _windowService.ShowConfirmContentDialog($"Delete {ProfileCategory.Name}", "Do you want to delete this category and all its profiles?"))
{ {
if (ProfileCategory.ProfileConfigurations.Any(c => c.IsBeingEdited)) if (ProfileCategory.ProfileConfigurations.Any(c => c.IsBeingEdited))
_profileEditorService.ChangeCurrentProfileConfiguration(null); await _router.Navigate("home");
_profileService.DeleteProfileCategory(ProfileCategory); _profileService.DeleteProfileCategory(ProfileCategory);
} }
} }

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -17,15 +18,15 @@ namespace Artemis.UI.Screens.Sidebar;
public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase
{ {
private readonly IProfileEditorService _profileEditorService; private readonly IRouter _router;
private readonly IProfileService _profileService; private readonly IProfileService _profileService;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<bool>? _isDisabled; private ObservableAsPropertyHelper<bool>? _isDisabled;
public SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration, IProfileService profileService, IProfileEditorService profileEditorService, IWindowService windowService) public SidebarProfileConfigurationViewModel(IRouter router, ProfileConfiguration profileConfiguration, IProfileService profileService, IWindowService windowService)
{ {
_router = router;
_profileService = profileService; _profileService = profileService;
_profileEditorService = profileEditorService;
_windowService = windowService; _windowService = windowService;
ProfileConfiguration = profileConfiguration; ProfileConfiguration = profileConfiguration;
@ -98,7 +99,7 @@ public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase
return; return;
if (ProfileConfiguration.IsBeingEdited) if (ProfileConfiguration.IsBeingEdited)
_profileEditorService.ChangeCurrentProfileConfiguration(null); await _router.Navigate("home");
_profileService.RemoveProfileConfiguration(ProfileConfiguration); _profileService.RemoveProfileConfiguration(ProfileConfiguration);
} }
@ -131,4 +132,9 @@ public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase
ProfileConfigurationExportModel export = _profileService.ExportProfile(ProfileConfiguration); ProfileConfigurationExportModel export = _profileService.ExportProfile(ProfileConfiguration);
_profileService.ImportProfile(ProfileConfiguration.Category, export, true, false, "copy"); _profileService.ImportProfile(ProfileConfiguration.Category, export, true, false, "copy");
} }
public bool Matches(string s)
{
return s.StartsWith("profile-editor") && s.EndsWith(ProfileConfiguration.ProfileId.ToString());
}
} }

View File

@ -1,35 +1,25 @@
using System; using System;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using DryIoc;
using Material.Icons; using Material.Icons;
using ReactiveUI;
namespace Artemis.UI.Screens.Sidebar; namespace Artemis.UI.Screens.Sidebar;
public class SidebarScreenViewModel<T> : SidebarScreenViewModel where T : MainScreenViewModel public class SidebarScreenViewModel : ViewModelBase
{ {
public SidebarScreenViewModel(MaterialIconKind icon, string displayName) : base(icon, displayName) public SidebarScreenViewModel(MaterialIconKind icon, string displayName, string path)
{
}
public override Type ScreenType => typeof(T);
public override MainScreenViewModel CreateInstance(IContainer container, IScreen screen)
{
return container.Resolve<T>(new object[] { screen });
}
}
public abstract class SidebarScreenViewModel : ViewModelBase
{
protected SidebarScreenViewModel(MaterialIconKind icon, string displayName)
{ {
Icon = icon; Icon = icon;
Path = path;
DisplayName = displayName; DisplayName = displayName;
} }
public MaterialIconKind Icon { get; } public MaterialIconKind Icon { get; }
public string Path { get; }
public abstract Type ScreenType { get; } public bool Matches(string? path)
public abstract MainScreenViewModel CreateInstance(IContainer container, IScreen screen); {
if (path == null)
return false;
return path.StartsWith(Path, StringComparison.InvariantCultureIgnoreCase);
}
} }

View File

@ -8,17 +8,12 @@ using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories; using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.Home;
using Artemis.UI.Screens.ProfileEditor;
using Artemis.UI.Screens.Settings;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.Workshop;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using DryIoc; using Avalonia.Threading;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using Material.Icons; using Material.Icons;
@ -28,50 +23,34 @@ namespace Artemis.UI.Screens.Sidebar;
public class SidebarViewModel : ActivatableViewModelBase public class SidebarViewModel : ActivatableViewModelBase
{ {
private readonly IScreen _hostScreen; private readonly IRouter _router;
private readonly IContainer _container;
private readonly IProfileEditorService _profileEditorService;
private readonly IProfileEditorVmFactory _profileEditorVmFactory;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private SidebarScreenViewModel? _selectedSidebarScreen; private SidebarScreenViewModel? _selectedSidebarScreen;
private ReadOnlyObservableCollection<SidebarCategoryViewModel> _sidebarCategories = new(new ObservableCollection<SidebarCategoryViewModel>()); private ReadOnlyObservableCollection<SidebarCategoryViewModel> _sidebarCategories = new(new ObservableCollection<SidebarCategoryViewModel>());
public SidebarViewModel(IScreen hostScreen, public SidebarViewModel(IRouter router, IProfileService profileService, IWindowService windowService, ISidebarVmFactory sidebarVmFactory)
IContainer container,
IProfileService profileService,
IWindowService windowService,
IProfileEditorService profileEditorService,
ISidebarVmFactory sidebarVmFactory,
IProfileEditorVmFactory profileEditorVmFactory)
{ {
_hostScreen = hostScreen; _router = router;
_container = container;
_windowService = windowService; _windowService = windowService;
_profileEditorService = profileEditorService;
_profileEditorVmFactory = profileEditorVmFactory;
SidebarScreens = new ObservableCollection<SidebarScreenViewModel> SidebarScreens = new ObservableCollection<SidebarScreenViewModel>
{ {
new SidebarScreenViewModel<HomeViewModel>(MaterialIconKind.Home, "Home"), new(MaterialIconKind.Home, "Home", "home"),
#if DEBUG #if DEBUG
new SidebarScreenViewModel<WorkshopViewModel>(MaterialIconKind.TestTube, "Workshop"), new(MaterialIconKind.TestTube, "Workshop", "workshop"),
#endif #endif
new SidebarScreenViewModel<SurfaceEditorViewModel>(MaterialIconKind.Devices, "Surface Editor"), new(MaterialIconKind.Devices, "Surface Editor", "surface-editor"),
new SidebarScreenViewModel<SettingsViewModel>(MaterialIconKind.Cog, "Settings") new(MaterialIconKind.Cog, "Settings", "settings")
}; };
AddCategory = ReactiveCommand.CreateFromTask(ExecuteAddCategory); AddCategory = ReactiveCommand.CreateFromTask(ExecuteAddCategory);
SourceList<ProfileCategory> profileCategories = new(); SourceList<ProfileCategory> profileCategories = new();
this.WhenAnyValue(vm => vm.SelectedSidebarScreen).WhereNotNull().Subscribe(NavigateToScreen);
this.WhenActivated(d => this.WhenActivated(d =>
{ {
this.WhenAnyObservable(vm => vm._hostScreen.Router.CurrentViewModel).WhereNotNull() _router.CurrentPath.WhereNotNull().Subscribe(r => SelectedSidebarScreen = SidebarScreens.FirstOrDefault(s => s.Matches(r))).DisposeWith(d);
.Subscribe(c => SelectedSidebarScreen = SidebarScreens.FirstOrDefault(s => s.ScreenType == c.GetType()))
.DisposeWith(d);
this.WhenAnyValue(vm => vm.SelectedSidebarScreen).WhereNotNull().Subscribe(NavigateToScreen);
this.WhenAnyObservable(vm => vm._profileEditorService.ProfileConfiguration).Subscribe(NavigateToProfile).DisposeWith(d);
Observable.FromEventPattern<ProfileCategoryEventArgs>(x => profileService.ProfileCategoryAdded += x, x => profileService.ProfileCategoryAdded -= x) Observable.FromEventPattern<ProfileCategoryEventArgs>(x => profileService.ProfileCategoryAdded += x, x => profileService.ProfileCategoryAdded -= x)
.Subscribe(e => profileCategories.Add(e.EventArgs.ProfileCategory)) .Subscribe(e => profileCategories.Add(e.EventArgs.ProfileCategory))
@ -127,21 +106,18 @@ public class SidebarViewModel : ActivatableViewModelBase
.ShowAsync(); .ShowAsync();
} }
private void NavigateToProfile(ProfileConfiguration? profile)
{
if (profile == null && _hostScreen.Router.GetCurrentViewModel() is ProfileEditorViewModel)
SelectedSidebarScreen = SidebarScreens.FirstOrDefault();
else if (profile != null && _hostScreen.Router.GetCurrentViewModel() is not ProfileEditorViewModel)
_hostScreen.Router.Navigate.Execute(_profileEditorVmFactory.ProfileEditorViewModel(_hostScreen));
}
private void NavigateToScreen(SidebarScreenViewModel sidebarScreenViewModel) private void NavigateToScreen(SidebarScreenViewModel sidebarScreenViewModel)
{ {
// If the current screen changed through external means and already matches, don't navigate again Dispatcher.UIThread.Invoke(async () =>
if (_hostScreen.Router.GetCurrentViewModel()?.GetType() == sidebarScreenViewModel.ScreenType) {
return; try
{
_hostScreen.Router.Navigate.Execute(sidebarScreenViewModel.CreateInstance(_container, _hostScreen)); await _router.Navigate(sidebarScreenViewModel.Path, new RouterNavigationOptions {IgnoreOnPartialMatch = true});
_profileEditorService.ChangeCurrentProfileConfiguration(null); }
catch (Exception e)
{
_windowService.ShowExceptionDialog("Navigation failed", e);
}
});
} }
} }

View File

@ -9,6 +9,7 @@ using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories; using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Extensions; using Artemis.UI.Extensions;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Avalonia; using Avalonia;
using ReactiveUI; using ReactiveUI;
@ -16,7 +17,7 @@ using SkiaSharp;
namespace Artemis.UI.Screens.SurfaceEditor; namespace Artemis.UI.Screens.SurfaceEditor;
public class SurfaceEditorViewModel : MainScreenViewModel public class SurfaceEditorViewModel : ActivatableViewModelBase, IMainScreenViewModel
{ {
private readonly IDeviceService _deviceService; private readonly IDeviceService _deviceService;
private readonly IDeviceVmFactory _deviceVmFactory; private readonly IDeviceVmFactory _deviceVmFactory;
@ -30,14 +31,13 @@ public class SurfaceEditorViewModel : MainScreenViewModel
private double _overlayOpacity; private double _overlayOpacity;
private bool _saving; private bool _saving;
public SurfaceEditorViewModel(IScreen hostScreen, public SurfaceEditorViewModel(ICoreService coreService,
ICoreService coreService,
IRgbService rgbService, IRgbService rgbService,
ISurfaceVmFactory surfaceVmFactory, ISurfaceVmFactory surfaceVmFactory,
ISettingsService settingsService, ISettingsService settingsService,
IDeviceVmFactory deviceVmFactory, IDeviceVmFactory deviceVmFactory,
IWindowService windowService, IWindowService windowService,
IDeviceService deviceService) : base(hostScreen, "surface-editor") IDeviceService deviceService)
{ {
_rgbService = rgbService; _rgbService = rgbService;
_surfaceVmFactory = surfaceVmFactory; _surfaceVmFactory = surfaceVmFactory;
@ -71,6 +71,8 @@ public class SurfaceEditorViewModel : MainScreenViewModel
}).DisposeWith(d); }).DisposeWith(d);
}); });
} }
public ViewModelBase? TitleBarViewModel => null;
public bool ColorDevices public bool ColorDevices
{ {

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
@ -15,7 +15,7 @@ using StrawberryShake;
namespace Artemis.UI.Screens.Workshop; namespace Artemis.UI.Screens.Workshop;
public class WorkshopViewModel : MainScreenViewModel public class WorkshopViewModel : ActivatableViewModelBase, IMainScreenViewModel
{ {
private readonly IWorkshopClient _workshopClient; private readonly IWorkshopClient _workshopClient;

View File

@ -9,6 +9,7 @@ using Artemis.UI.DefaultTypes.PropertyInput;
using Artemis.UI.Services.Interfaces; using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared.DefaultTypes.DataModel.Display; using Artemis.UI.Shared.DefaultTypes.DataModel.Display;
using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Providers;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.PropertyInput; using Artemis.UI.Shared.Services.PropertyInput;
@ -24,12 +25,14 @@ public class RegistrationService : IRegistrationService
private readonly IDataModelUIService _dataModelUIService; private readonly IDataModelUIService _dataModelUIService;
private readonly IInputService _inputService; private readonly IInputService _inputService;
private readonly IContainer _container; private readonly IContainer _container;
private readonly IRouter _router;
private readonly INodeService _nodeService; private readonly INodeService _nodeService;
private readonly IPropertyInputService _propertyInputService; private readonly IPropertyInputService _propertyInputService;
private readonly IWebServerService _webServerService; private readonly IWebServerService _webServerService;
private bool _registeredBuiltInPropertyEditors; private bool _registeredBuiltInPropertyEditors;
public RegistrationService(IContainer container, public RegistrationService(IContainer container,
IRouter router,
IInputService inputService, IInputService inputService,
IPropertyInputService propertyInputService, IPropertyInputService propertyInputService,
IProfileEditorService profileEditorService, IProfileEditorService profileEditorService,
@ -40,6 +43,7 @@ public class RegistrationService : IRegistrationService
) )
{ {
_container = container; _container = container;
_router = router;
_inputService = inputService; _inputService = inputService;
_propertyInputService = propertyInputService; _propertyInputService = propertyInputService;
_nodeService = nodeService; _nodeService = nodeService;
@ -47,10 +51,16 @@ public class RegistrationService : IRegistrationService
_webServerService = webServerService; _webServerService = webServerService;
CreateCursorResources(); CreateCursorResources();
RegisterRoutes();
RegisterBuiltInNodeTypes(); RegisterBuiltInNodeTypes();
RegisterControllers(); RegisterControllers();
} }
private void RegisterRoutes()
{
_router.Routes.AddRange(Routing.Routes.ArtemisRoutes);
}
private void CreateCursorResources() private void CreateCursorResources()
{ {
ICursorProvider? cursorProvider = _container.Resolve<ICursorProvider>(IfUnresolved.ReturnDefault); ICursorProvider? cursorProvider = _container.Resolve<ICursorProvider>(IfUnresolved.ReturnDefault);

View File

@ -1,47 +1,28 @@
using System; using System;
using System.Linq; using System.Threading.Tasks;
using Artemis.UI.Screens.Settings; using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.MainWindow; using Artemis.UI.Shared.Services.MainWindow;
using ReactiveUI;
namespace Artemis.UI.Services.Updating; namespace Artemis.UI.Services.Updating;
public class BasicUpdateNotificationProvider : IUpdateNotificationProvider public class BasicUpdateNotificationProvider : IUpdateNotificationProvider
{ {
private readonly Func<IScreen, SettingsViewModel> _getSettingsViewModel;
private readonly IMainWindowService _mainWindowService; private readonly IMainWindowService _mainWindowService;
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly IRouter _router;
private Action? _available; private Action? _available;
private Action? _installed; private Action? _installed;
public BasicUpdateNotificationProvider(INotificationService notificationService, IMainWindowService mainWindowService, Func<IScreen, SettingsViewModel> getSettingsViewModel) public BasicUpdateNotificationProvider(INotificationService notificationService, IMainWindowService mainWindowService, IRouter router)
{ {
_notificationService = notificationService; _notificationService = notificationService;
_mainWindowService = mainWindowService; _mainWindowService = mainWindowService;
_getSettingsViewModel = getSettingsViewModel; _router = router;
} }
/// <inheritdoc /> private void ShowAvailable(Guid releaseId, string releaseVersion)
public void ShowNotification(Guid releaseId, string releaseVersion)
{
if (_mainWindowService.IsMainWindowOpen)
ShowAvailable(releaseVersion);
else
_mainWindowService.MainWindowOpened += (_, _) => ShowAvailable(releaseVersion);
}
/// <inheritdoc />
public void ShowInstalledNotification(string installedVersion)
{
if (_mainWindowService.IsMainWindowOpen)
ShowInstalled(installedVersion);
else
_mainWindowService.MainWindowOpened += (_, _) => ShowInstalled(installedVersion);
}
private void ShowAvailable(string releaseVersion)
{ {
_available?.Invoke(); _available?.Invoke();
_available = _notificationService.CreateNotification() _available = _notificationService.CreateNotification()
@ -49,7 +30,7 @@ public class BasicUpdateNotificationProvider : IUpdateNotificationProvider
.WithMessage($"Artemis {releaseVersion} has been released") .WithMessage($"Artemis {releaseVersion} has been released")
.WithSeverity(NotificationSeverity.Success) .WithSeverity(NotificationSeverity.Success)
.WithTimeout(TimeSpan.FromSeconds(15)) .WithTimeout(TimeSpan.FromSeconds(15))
.HavingButton(b => b.WithText("View release").WithAction(() => ViewRelease(releaseVersion))) .HavingButton(b => b.WithText("View release").WithAction(() => ViewRelease(releaseId)))
.Show(); .Show();
} }
@ -61,28 +42,36 @@ public class BasicUpdateNotificationProvider : IUpdateNotificationProvider
.WithMessage($"Artemis {installedVersion} has been installed.") .WithMessage($"Artemis {installedVersion} has been installed.")
.WithSeverity(NotificationSeverity.Success) .WithSeverity(NotificationSeverity.Success)
.WithTimeout(TimeSpan.FromSeconds(15)) .WithTimeout(TimeSpan.FromSeconds(15))
.HavingButton(b => b.WithText("View release").WithAction(() => ViewRelease(installedVersion))) .HavingButton(b => b.WithText("View release").WithAction(async () => await ViewRelease(null)))
.Show(); .Show();
} }
private void ViewRelease(string version) private async Task ViewRelease(Guid? releaseId)
{ {
_installed?.Invoke(); _installed?.Invoke();
_available?.Invoke(); _available?.Invoke();
if (_mainWindowService.HostScreen == null) if (releaseId != null)
return; await _router.Navigate($"settings/releases/{releaseId}");
else
await _router.Navigate("settings/releases");
}
// TODO: When proper routing has been implemented, use that here /// <inheritdoc />
// Create a settings VM to navigate to public void ShowNotification(Guid releaseId, string releaseVersion)
SettingsViewModel settingsViewModel = _getSettingsViewModel(_mainWindowService.HostScreen); {
// Get the release tab if (_mainWindowService.IsMainWindowOpen)
ReleasesTabViewModel releaseTabViewModel = (ReleasesTabViewModel) settingsViewModel.SettingTabs.First(t => t is ReleasesTabViewModel); ShowAvailable(releaseId, releaseVersion);
else
_mainWindowService.MainWindowOpened += (_, _) => ShowAvailable(releaseId, releaseVersion);
}
// Navigate to the settings VM /// <inheritdoc />
_mainWindowService.HostScreen.Router.Navigate.Execute(settingsViewModel); public void ShowInstalledNotification(string installedVersion)
// Navigate to the release tab {
releaseTabViewModel.PreselectVersion = version; if (_mainWindowService.IsMainWindowOpen)
settingsViewModel.SelectedTab = releaseTabViewModel; ShowInstalled(installedVersion);
else
_mainWindowService.MainWindowOpened += (_, _) => ShowInstalled(installedVersion);
} }
} }

View File

@ -3,6 +3,7 @@ query GetReleaseById($id: UUID!) {
branch branch
commit commit
version version
createdAt
previousRelease { previousRelease {
version version
} }