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:
commit
229d93901b
@ -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++)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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}" />
|
||||||
|
|||||||
@ -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}" />
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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 />
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
21
src/Artemis.UI.Shared/Exceptions/ArtemisRoutingException.cs
Normal file
21
src/Artemis.UI.Shared/Exceptions/ArtemisRoutingException.cs
Normal 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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Artemis.UI.Shared/Routing/Routable/RoutableScreen.cs
Normal file
24
src/Artemis.UI.Shared/Routing/Routable/RoutableScreen.cs
Normal 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);
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/Artemis.UI.Shared/Routing/Route/Route.cs
Normal file
22
src/Artemis.UI.Shared/Routing/Route/Route.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/Artemis.UI.Shared/Routing/Route/RouteRegistration.cs
Normal file
36
src/Artemis.UI.Shared/Routing/Route/RouteRegistration.cs
Normal 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; }
|
||||||
|
|
||||||
|
}
|
||||||
115
src/Artemis.UI.Shared/Routing/Route/RouteResolution.cs
Normal file
115
src/Artemis.UI.Shared/Routing/Route/RouteResolution.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/Artemis.UI.Shared/Routing/Route/RouteSegment.cs
Normal file
60
src/Artemis.UI.Shared/Routing/Route/RouteSegment.cs
Normal 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();
|
||||||
|
}
|
||||||
62
src/Artemis.UI.Shared/Routing/Router/IRouter.cs
Normal file
62
src/Artemis.UI.Shared/Routing/Router/IRouter.cs
Normal 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();
|
||||||
|
}
|
||||||
121
src/Artemis.UI.Shared/Routing/Router/Navigation.cs
Normal file
121
src/Artemis.UI.Shared/Routing/Router/Navigation.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/Artemis.UI.Shared/Routing/Router/NavigationArguments.cs
Normal file
49
src/Artemis.UI.Shared/Routing/Router/NavigationArguments.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
167
src/Artemis.UI.Shared/Routing/Router/Router.cs
Normal file
167
src/Artemis.UI.Shared/Routing/Router/Router.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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}" />
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
24
src/Artemis.UI/Converters/SubstringConverter.cs
Normal file
24
src/Artemis.UI/Converters/SubstringConverter.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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">
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
29
src/Artemis.UI/PageFactory.cs
Normal file
29
src/Artemis.UI/PageFactory.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/Artemis.UI/Routing/Routes.cs
Normal file
38
src/Artemis.UI/Routing/Routes.cs
Normal 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}")
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
8
src/Artemis.UI/Screens/IMainScreenViewModel.cs
Normal file
8
src/Artemis.UI/Screens/IMainScreenViewModel.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
using Artemis.UI.Shared;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens;
|
||||||
|
|
||||||
|
public interface IMainScreenViewModel
|
||||||
|
{
|
||||||
|
ViewModelBase? TitleBarViewModel { get; }
|
||||||
|
}
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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; }
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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}" />
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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; }
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
20
src/Artemis.UI/Screens/Settings/SettingsTab.cs
Normal file
20
src/Artemis.UI/Screens/Settings/SettingsTab.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -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()}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -32,6 +32,7 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsRepeater.ItemTemplate>
|
</ItemsRepeater.ItemTemplate>
|
||||||
</ItemsRepeater>
|
</ItemsRepeater>
|
||||||
|
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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; }
|
||||||
|
}
|
||||||
@ -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>
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,6 +3,7 @@ query GetReleaseById($id: UUID!) {
|
|||||||
branch
|
branch
|
||||||
commit
|
commit
|
||||||
version
|
version
|
||||||
|
createdAt
|
||||||
previousRelease {
|
previousRelease {
|
||||||
version
|
version
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user