mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 11:56:30 +00:00
UI - Added routing (#802)
ColorGradient - Fixed GetColor returning the first stop if position was after the last stop
This commit is contained in:
1 parent
79309dcee7
commit
962b13c6ed
77 files changed
+2034
-854
No files matched your search
@@ -1,19 +1,19 @@
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Screens.StartupWizard;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Avalonia.Threading;
|
||||
using ReactiveUI;
|
||||
|
||||
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
|
||||
if (!settingsService.GetSetting("UI.SetupWizardCompleted", false).Value)
|
||||
Dispatcher.UIThread.InvokeAsync(async () => await windowService.ShowDialogAsync<StartupWizardViewModel, bool>());
|
||||
}
|
||||
|
||||
public ViewModelBase? TitleBarViewModel => null;
|
||||
}
|
||||
@@ -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.Sidebar;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Newtonsoft.Json;
|
||||
@@ -21,7 +22,7 @@ namespace Artemis.UI.Screens.ProfileEditor.MenuBar;
|
||||
|
||||
public class MenuBarViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IRouter _router;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
@@ -36,9 +37,9 @@ public class MenuBarViewModel : ActivatableViewModelBase
|
||||
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
|
||||
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;
|
||||
_profileService = profileService;
|
||||
_settingsService = settingsService;
|
||||
@@ -182,7 +183,7 @@ public class MenuBarViewModel : ActivatableViewModelBase
|
||||
return;
|
||||
|
||||
if (ProfileConfiguration.IsBeingEdited)
|
||||
_profileEditorService.ChangeCurrentProfileConfiguration(null);
|
||||
await _router.Navigate("home");
|
||||
_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.Commands;
|
||||
using Avalonia;
|
||||
using Avalonia.ReactiveUI;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree;
|
||||
@@ -71,7 +72,7 @@ public abstract class TreeItemViewModel : ActivatableViewModelBase
|
||||
CreateTreeItems();
|
||||
});
|
||||
|
||||
this.WhenAnyValue(vm => vm.IsFlyoutOpen).Subscribe(UpdateCanPaste);
|
||||
this.WhenAnyValue(vm => vm.IsFlyoutOpen).ObserveOn(AvaloniaScheduler.Instance).Subscribe(UpdateCanPaste);
|
||||
}
|
||||
|
||||
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.Never<EventPattern<object>>())
|
||||
.Switch()
|
||||
.ObserveOn(Shared.UI.BackgroundScheduler)
|
||||
.Subscribe(_ => UpdatePropertyGroups())
|
||||
.DisposeWith(d);
|
||||
this.WhenAnyValue(vm => vm.ProfileElement)
|
||||
@@ -90,10 +91,11 @@ public class PropertiesViewModel : ActivatableViewModelBase
|
||||
? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x)
|
||||
: Observable.Never<EventPattern<object>>())
|
||||
.Switch()
|
||||
.ObserveOn(Shared.UI.BackgroundScheduler)
|
||||
.Subscribe(_ => UpdatePropertyGroups())
|
||||
.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());
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,11 @@
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</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">
|
||||
<shared:ProfileConfigurationIcon ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}" Width="18" Height="18" Margin="0 0 5 0" />
|
||||
<TextBlock Text="{CompiledBinding ProfileConfiguration.Name}" />
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.PanAndZoom;
|
||||
using Avalonia.LogicalTree;
|
||||
@@ -17,7 +18,7 @@ public partial class LayerShapeVisualizerView : ReactiveUserControl<LayerShapeVi
|
||||
public LayerShapeVisualizerView()
|
||||
{
|
||||
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>
|
||||
<converters:DoubleToGridLengthConverter x:Key="DoubleToGridLengthConverter" />
|
||||
</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>
|
||||
<KeyBinding Command="{CompiledBinding History.Undo}" Gesture="Ctrl+Z" />
|
||||
<KeyBinding Command="{CompiledBinding History.Redo}" Gesture="Ctrl+Y" />
|
||||
@@ -30,6 +22,12 @@
|
||||
<KeyBinding Command="{Binding TitleBarViewModel.MenuBarViewModel.CycleFocusMode}" Gesture="F" />
|
||||
</UserControl.KeyBindings>
|
||||
<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">
|
||||
<Setter Property="MinWidth" Value="4" />
|
||||
<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">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Border Grid.Column="0">
|
||||
<ItemsControl ItemsSource="{CompiledBinding Tools}">
|
||||
<ItemsControl ItemsSource="{CompiledBinding Tools}" IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="shared:IToolViewModel">
|
||||
<ToggleButton Classes="icon-button editor-sidebar-button"
|
||||
@@ -83,14 +81,15 @@
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</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>
|
||||
</Border>
|
||||
|
||||
|
||||
<GridSplitter Grid.Row="1" Classes="editor-grid-splitter-horizontal" />
|
||||
|
||||
|
||||
<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}" />
|
||||
<Border Classes="suspended-editing">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="16">
|
||||
@@ -109,9 +108,9 @@
|
||||
</Panel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
|
||||
<GridSplitter Grid.Row="0" Grid.Column="1" Classes="editor-grid-splitter-vertical" />
|
||||
|
||||
|
||||
<Grid Grid.Row="0" Grid.Column="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
@@ -119,22 +118,25 @@
|
||||
<RowDefinition Height="{CompiledBinding ConditionsHeight.Value, Mode=TwoWay, Converter={StaticResource DoubleToGridLengthConverter}}" />
|
||||
</Grid.RowDefinitions>
|
||||
<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}" />
|
||||
<Border Classes="suspended-editing" />
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
|
||||
<GridSplitter Grid.Row="1" Classes="editor-grid-splitter-horizontal" />
|
||||
|
||||
|
||||
<Border Grid.Row="2" Classes="card card-condensed" Margin="4">
|
||||
<Panel>
|
||||
<Panel IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<ContentControl Content="{CompiledBinding DisplayConditionScriptViewModel}" />
|
||||
<Border Classes="suspended-editing" />
|
||||
</Panel>
|
||||
</Border>
|
||||
</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>
|
||||
</UserControl>
|
||||
@@ -4,6 +4,8 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
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.StatusBar;
|
||||
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.ProfileEditor;
|
||||
using Avalonia.Threading;
|
||||
@@ -20,22 +24,18 @@ using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor;
|
||||
|
||||
public class ProfileEditorViewModel : MainScreenViewModel
|
||||
public class ProfileEditorViewModel : RoutableScreen<object, ProfileEditorViewModelParameters>, IMainScreenViewModel
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly SourceList<IToolViewModel> _tools;
|
||||
private DisplayConditionScriptViewModel? _displayConditionScriptViewModel;
|
||||
private ObservableAsPropertyHelper<ProfileEditorHistory?>? _history;
|
||||
private ObservableAsPropertyHelper<ProfileConfiguration?>? _profileConfiguration;
|
||||
private ProfileTreeViewModel? _profileTreeViewModel;
|
||||
private PropertiesViewModel? _propertiesViewModel;
|
||||
private StatusBarViewModel? _statusBarViewModel;
|
||||
private ProfileConfiguration? _profileConfiguration;
|
||||
private ObservableAsPropertyHelper<bool>? _suspendedEditing;
|
||||
private VisualEditorViewModel? _visualEditorViewModel;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProfileEditorViewModel(IScreen hostScreen,
|
||||
public ProfileEditorViewModel(IProfileService profileService,
|
||||
IProfileEditorService profileEditorService,
|
||||
ISettingsService settingsService,
|
||||
VisualEditorViewModel visualEditorViewModel,
|
||||
@@ -46,8 +46,8 @@ public class ProfileEditorViewModel : MainScreenViewModel
|
||||
StatusBarViewModel statusBarViewModel,
|
||||
IEnumerable<IToolViewModel> toolViewModels,
|
||||
IMainWindowService mainWindowService)
|
||||
: base(hostScreen, "profile-editor")
|
||||
{
|
||||
_profileService = profileService;
|
||||
_profileEditorService = profileEditorService;
|
||||
_settingsService = settingsService;
|
||||
|
||||
@@ -62,9 +62,14 @@ public class ProfileEditorViewModel : MainScreenViewModel
|
||||
Tools = tools;
|
||||
visualEditorViewModel.SetTools(_tools);
|
||||
|
||||
StatusBarViewModel = statusBarViewModel;
|
||||
VisualEditorViewModel = visualEditorViewModel;
|
||||
ProfileTreeViewModel = profileTreeViewModel;
|
||||
PropertiesViewModel = propertiesViewModel;
|
||||
DisplayConditionScriptViewModel = displayConditionScriptViewModel;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
_profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d);
|
||||
_history = profileEditorService.History.ToProperty(this, vm => vm.History).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)
|
||||
toolViewModel.Dispose();
|
||||
}).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;
|
||||
@@ -92,38 +90,19 @@ public class ProfileEditorViewModel : MainScreenViewModel
|
||||
ToggleAutoSuspend = ReactiveCommand.Create(ExecuteToggleAutoSuspend);
|
||||
}
|
||||
|
||||
public VisualEditorViewModel? VisualEditorViewModel
|
||||
public ProfileConfiguration? ProfileConfiguration
|
||||
{
|
||||
get => _visualEditorViewModel;
|
||||
set => RaiseAndSetIfChanged(ref _visualEditorViewModel, value);
|
||||
get => _profileConfiguration;
|
||||
set => RaiseAndSetIfChanged(ref _profileConfiguration, value);
|
||||
}
|
||||
|
||||
public ProfileTreeViewModel? ProfileTreeViewModel
|
||||
{
|
||||
get => _profileTreeViewModel;
|
||||
set => RaiseAndSetIfChanged(ref _profileTreeViewModel, value);
|
||||
}
|
||||
|
||||
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 VisualEditorViewModel? VisualEditorViewModel { get; }
|
||||
public ProfileTreeViewModel? ProfileTreeViewModel { get; }
|
||||
public PropertiesViewModel? PropertiesViewModel { get; }
|
||||
public DisplayConditionScriptViewModel? DisplayConditionScriptViewModel { get; }
|
||||
public StatusBarViewModel? StatusBarViewModel { get; }
|
||||
|
||||
public ReadOnlyObservableCollection<IToolViewModel> Tools { get; }
|
||||
public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value;
|
||||
public ProfileEditorHistory? History => _history?.Value;
|
||||
public bool SuspendedEditing => _suspendedEditing?.Value ?? false;
|
||||
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> ToggleAutoSuspend { get; }
|
||||
|
||||
public void OpenUrl(string url)
|
||||
{
|
||||
Utilities.OpenUrl(url);
|
||||
}
|
||||
|
||||
private void ExecuteToggleSuspend()
|
||||
{
|
||||
_profileEditorService.ChangeSuspendedEditing(!SuspendedEditing);
|
||||
@@ -177,4 +151,41 @@ public class ProfileEditorViewModel : MainScreenViewModel
|
||||
if (_settingsService.GetSetting("ProfileEditor.AutoSuspend", true).Value)
|
||||
_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:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:reactiveUi="http://reactiveui.net"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
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:Class="Artemis.UI.Screens.Root.RootView">
|
||||
<reactiveUi:RoutedViewHost Router="{CompiledBinding Router}">
|
||||
<reactiveUi:RoutedViewHost.PageTransition>
|
||||
<CompositePageTransition></CompositePageTransition>
|
||||
</reactiveUi:RoutedViewHost.PageTransition>
|
||||
</reactiveUi:RoutedViewHost>
|
||||
<controls:Frame Name="RootFrame" IsNavigationStackEnabled="False" CacheSize="0">
|
||||
<controls:Frame.NavigationPageFactory>
|
||||
<ui:PageFactory/>
|
||||
</controls:Frame.NavigationPageFactory>
|
||||
</controls:Frame>
|
||||
</UserControl>
|
||||
@@ -1,5 +1,8 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Avalonia.Threading;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Root;
|
||||
|
||||
@@ -8,6 +11,18 @@ public partial class RootView : ReactiveUserControl<RootViewModel>
|
||||
public RootView()
|
||||
{
|
||||
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.Linq;
|
||||
using System.Reactive;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.DryIoc.Factories;
|
||||
using Artemis.UI.Models;
|
||||
using Artemis.UI.Screens.Sidebar;
|
||||
using Artemis.UI.Services.Interfaces;
|
||||
using Artemis.UI.Services.Updating;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.MainWindow;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Threading;
|
||||
using ReactiveUI;
|
||||
|
||||
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 IDebugService _debugService;
|
||||
private readonly DefaultTitleBarViewModel _defaultTitleBarViewModel;
|
||||
private readonly IClassicDesktopStyleApplicationLifetime _lifeTime;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ISidebarVmFactory _sidebarVmFactory;
|
||||
private readonly IUpdateService _updateService;
|
||||
private readonly IWindowService _windowService;
|
||||
private SidebarViewModel? _sidebarViewModel;
|
||||
private ViewModelBase? _titleBarViewModel;
|
||||
|
||||
public RootViewModel(ICoreService coreService,
|
||||
public RootViewModel(IRouter router,
|
||||
ICoreService coreService,
|
||||
ISettingsService settingsService,
|
||||
IRegistrationService registrationService,
|
||||
IWindowService windowService,
|
||||
IMainWindowService mainWindowService,
|
||||
IDebugService debugService,
|
||||
IUpdateService updateService,
|
||||
DefaultTitleBarViewModel defaultTitleBarViewModel,
|
||||
ISidebarVmFactory sidebarVmFactory)
|
||||
SidebarViewModel sidebarViewModel,
|
||||
DefaultTitleBarViewModel defaultTitleBarViewModel)
|
||||
{
|
||||
Router = new RoutingState();
|
||||
WindowSizeSetting = settingsService.GetSetting<WindowSize?>("WindowSize");
|
||||
SidebarViewModel = sidebarViewModel;
|
||||
|
||||
_router = router;
|
||||
_coreService = coreService;
|
||||
_settingsService = settingsService;
|
||||
_windowService = windowService;
|
||||
_debugService = debugService;
|
||||
_updateService = updateService;
|
||||
_defaultTitleBarViewModel = defaultTitleBarViewModel;
|
||||
_sidebarVmFactory = sidebarVmFactory;
|
||||
_lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!;
|
||||
|
||||
router.SetRoot(this);
|
||||
mainWindowService.ConfigureMainWindowProvider(this);
|
||||
mainWindowService.HostScreen = this;
|
||||
|
||||
DisplayAccordingToSettings();
|
||||
OpenScreen = ReactiveCommand.Create<string>(ExecuteOpenScreen);
|
||||
OpenDebugger = ReactiveCommand.CreateFromTask(ExecuteOpenDebugger);
|
||||
Exit = ReactiveCommand.CreateFromTask(ExecuteExit);
|
||||
|
||||
Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel);
|
||||
this.WhenAnyValue(vm => vm.Screen).Subscribe(UpdateTitleBarViewModel);
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
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<Unit, Unit> OpenDebugger { get; }
|
||||
public ReactiveCommand<Unit, Unit> Exit { get; }
|
||||
|
||||
public SidebarViewModel? SidebarViewModel
|
||||
{
|
||||
get => _sidebarViewModel;
|
||||
set => RaiseAndSetIfChanged(ref _sidebarViewModel, value);
|
||||
}
|
||||
|
||||
public ViewModelBase? TitleBarViewModel
|
||||
{
|
||||
get => _titleBarViewModel;
|
||||
set => RaiseAndSetIfChanged(ref _titleBarViewModel, value);
|
||||
}
|
||||
|
||||
private void UpdateTitleBarViewModel(IRoutableViewModel? viewModel)
|
||||
public void GoBack()
|
||||
{
|
||||
if (viewModel is MainScreenViewModel mainScreenViewModel && mainScreenViewModel.TitleBarViewModel != null)
|
||||
TitleBarViewModel = mainScreenViewModel.TitleBarViewModel;
|
||||
_router.GoBack();
|
||||
}
|
||||
|
||||
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
|
||||
TitleBarViewModel = _defaultTitleBarViewModel;
|
||||
}
|
||||
@@ -104,8 +109,6 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
||||
{
|
||||
WindowSizeSetting?.Save();
|
||||
_lifeTime.MainWindow = null;
|
||||
SidebarViewModel = null;
|
||||
Router.NavigateAndReset.Execute(new EmptyViewModel(this, "blank")).Subscribe();
|
||||
OnMainWindowClosed();
|
||||
}
|
||||
|
||||
@@ -127,14 +130,9 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
||||
_windowService.ShowWindow<SplashViewModel>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RoutingState Router { get; }
|
||||
|
||||
public static PluginSetting<WindowSize?>? WindowSizeSetting { get; private set; }
|
||||
|
||||
#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
|
||||
MainWindowOpened += OnEventHandler;
|
||||
@@ -142,10 +140,9 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
||||
|
||||
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;
|
||||
// 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)
|
||||
{
|
||||
SidebarViewModel = _sidebarVmFactory.SidebarViewModel(this);
|
||||
_lifeTime.MainWindow = new MainWindow {DataContext = this};
|
||||
_lifeTime.MainWindow.Show();
|
||||
_lifeTime.MainWindow.Closing += CurrentMainWindowOnClosing;
|
||||
@@ -237,12 +233,4 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
internal class EmptyViewModel : MainScreenViewModel
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public EmptyViewModel(IScreen hostScreen, string urlPathSegment) : base(hostScreen, urlPathSegment)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
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"
|
||||
x:Class="Artemis.UI.Screens.Settings.SettingsView"
|
||||
x:DataType="settings:SettingsViewModel">
|
||||
<Border Classes="router-container">
|
||||
<TabControl Margin="12" ItemsSource="{CompiledBinding SettingTabs}" SelectedItem="{CompiledBinding SelectedTab}">
|
||||
<TabControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{CompiledBinding DisplayName}" />
|
||||
</DataTemplate>
|
||||
</TabControl.ItemTemplate>
|
||||
</TabControl>
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<TabStrip Grid.Row="0" Margin="12" ItemsSource="{CompiledBinding SettingTabs}" SelectedItem="{CompiledBinding SelectedTab}">
|
||||
<TabStrip.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{CompiledBinding Name}" />
|
||||
</DataTemplate>
|
||||
</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>
|
||||
</UserControl>
|
||||
@@ -1,5 +1,11 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using Artemis.UI.Shared;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Media.Animation;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Settings;
|
||||
|
||||
@@ -8,6 +14,11 @@ public partial class SettingsView : ReactiveUserControl<SettingsViewModel>
|
||||
public SettingsView()
|
||||
{
|
||||
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.Routing;
|
||||
using ReactiveUI;
|
||||
|
||||
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,
|
||||
GeneralTabViewModel generalTabViewModel,
|
||||
PluginsTabViewModel pluginsTabViewModel,
|
||||
DevicesTabViewModel devicesTabViewModel,
|
||||
ReleasesTabViewModel releasesTabViewModel,
|
||||
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
|
||||
public SettingsViewModel(IRouter router)
|
||||
{
|
||||
SettingTabs = new ObservableCollection<ActivatableViewModelBase>
|
||||
_router = router;
|
||||
SettingTabs = new ObservableCollection<SettingsTab>
|
||||
{
|
||||
generalTabViewModel,
|
||||
pluginsTabViewModel,
|
||||
devicesTabViewModel,
|
||||
releasesTabViewModel,
|
||||
aboutTabViewModel
|
||||
new("general", "General"),
|
||||
new("plugins", "Plugins"),
|
||||
new("devices", "Devices"),
|
||||
new("releases", "Releases"),
|
||||
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;
|
||||
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>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -49,7 +49,7 @@ public class PluginsTabViewModel : ActivatableViewModelBase
|
||||
|
||||
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)
|
||||
.Subscribe(a => plugins.Add(a.EventArgs.Plugin))
|
||||
.DisposeWith(d);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:ui="clr-namespace:Artemis.UI"
|
||||
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
|
||||
x:Class="Artemis.UI.Screens.Settings.ReleasesTabView"
|
||||
x:DataType="settings:ReleasesTabViewModel">
|
||||
@@ -34,31 +35,16 @@
|
||||
</controls:HyperlinkButton>
|
||||
</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">
|
||||
<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>
|
||||
<ListBox ItemsSource="{CompiledBinding ReleaseViewModels}" SelectedItem="{CompiledBinding SelectedReleaseViewModel}"/>
|
||||
</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>
|
||||
</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.Threading;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Settings;
|
||||
|
||||
@@ -8,6 +12,22 @@ public partial class ReleasesTabView : ReactiveUserControl<ReleasesTabViewModel>
|
||||
public ReleasesTabView()
|
||||
{
|
||||
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.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.UI.DryIoc.Factories;
|
||||
using Artemis.UI.Extensions;
|
||||
using Artemis.UI.Screens.Settings.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.Builders;
|
||||
using Artemis.WebClient.Updating;
|
||||
@@ -21,47 +22,46 @@ using StrawberryShake;
|
||||
|
||||
namespace Artemis.UI.Screens.Settings;
|
||||
|
||||
public class ReleasesTabViewModel : ActivatableViewModelBase
|
||||
public class ReleasesTabViewModel : RoutableScreen<ReleaseDetailsViewModel>
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IUpdateService _updateService;
|
||||
private readonly IUpdatingClient _updatingClient;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly IRouter _router;
|
||||
private readonly SourceList<IGetReleases_PublishedReleases_Nodes> _releases;
|
||||
private IGetReleases_PublishedReleases_PageInfo? _lastPageInfo;
|
||||
private bool _loading;
|
||||
private IGetReleases_PublishedReleases_PageInfo? _lastPageInfo;
|
||||
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;
|
||||
_updateService = updateService;
|
||||
_updatingClient = updatingClient;
|
||||
_notificationService = notificationService;
|
||||
_router = router;
|
||||
|
||||
_releases = new SourceList<IGetReleases_PublishedReleases_Nodes>();
|
||||
_releases.Connect()
|
||||
.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)
|
||||
.Bind(out ReadOnlyObservableCollection<ReleaseViewModel> releaseViewModels)
|
||||
.Subscribe();
|
||||
|
||||
DisplayName = "Releases";
|
||||
RecycleScreen = false;
|
||||
ReleaseViewModels = releaseViewModels;
|
||||
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 string Channel { get; }
|
||||
public string? PreselectVersion { get; set; }
|
||||
|
||||
public ReleaseViewModel? SelectedReleaseViewModel
|
||||
{
|
||||
@@ -109,4 +109,22 @@ public class ReleasesTabViewModel : ActivatableViewModelBase
|
||||
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: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"
|
||||
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseView"
|
||||
x:DataType="updating:ReleaseViewModel">
|
||||
<UserControl.Resources>
|
||||
<converters:BytesToStringConverter x:Key="BytesToStringConverter" />
|
||||
</UserControl.Resources>
|
||||
<UserControl.Styles>
|
||||
<Style Selector=":is(Control).fade-in">
|
||||
<Setter Property="Opacity" Value="0"></Setter>
|
||||
</Style>
|
||||
<Style Selector=":is(Control).fade-in[IsVisible=True]">
|
||||
<Style.Animations>
|
||||
<Animation Duration="0:00:00.250" FillMode="Forward" Easing="CubicEaseInOut">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="Opacity" Value="0.0" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="Opacity" Value="1.0" />
|
||||
</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>
|
||||
<Panel>
|
||||
<Grid Margin="4" IsVisible="{CompiledBinding ShowStatusIndicator}" RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="{CompiledBinding Release.Version}" VerticalAlignment="Center" FontWeight="SemiBold" />
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="{CompiledBinding Release.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 Release.Version}" VerticalAlignment="Center" />
|
||||
<TextBlock Text="{CompiledBinding Release.CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</UserControl>
|
||||
@@ -1,19 +1,18 @@
|
||||
using Avalonia.Input;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Settings.Updating;
|
||||
|
||||
public partial class ReleaseView : ReactiveUserControl<ReleaseViewModel>
|
||||
public partial class ReleaseView : UserControl
|
||||
{
|
||||
public ReleaseView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
|
||||
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
private void InitializeComponent()
|
||||
{
|
||||
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.UI.Extensions;
|
||||
using Artemis.UI.Services.Updating;
|
||||
using Artemis.UI.Shared;
|
||||
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 ReleaseViewModel : ActivatableViewModelBase
|
||||
public class ReleaseViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly IUpdateService _updateService;
|
||||
private readonly Platform _updatePlatform;
|
||||
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 IGetReleases_PublishedReleases_Nodes Release { get; }
|
||||
|
||||
public ReleaseViewModel(Guid releaseId,
|
||||
string version,
|
||||
DateTimeOffset createdAt,
|
||||
ILogger logger,
|
||||
IUpdatingClient updatingClient,
|
||||
INotificationService notificationService,
|
||||
IUpdateService updateService)
|
||||
public ReleaseViewModel(IUpdateService updateService, IGetReleases_PublishedReleases_Nodes release)
|
||||
{
|
||||
_logger = logger;
|
||||
_updatingClient = updatingClient;
|
||||
_notificationService = notificationService;
|
||||
_updateService = updateService;
|
||||
|
||||
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);
|
||||
});
|
||||
Release = release;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
Utilities.OpenUrl($"https://github.com/Artemis-RGB/Artemis/commit/{Commit}");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
public bool IsCurrentVersion => Release.Version == Constants.CurrentVersion;
|
||||
public bool IsPreviousVersion => Release.Version == _updateService.PreviousVersion;
|
||||
public bool ShowStatusIndicator => IsCurrentVersion || IsPreviousVersion;
|
||||
}
|
||||
@@ -149,7 +149,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase<ProfileConf
|
||||
return;
|
||||
|
||||
if (_profileConfiguration.IsBeingEdited)
|
||||
_profileEditorService.ChangeCurrentProfileConfiguration(null);
|
||||
await _profileEditorService.ChangeCurrentProfileConfiguration(null);
|
||||
_profileService.RemoveProfileConfiguration(_profileConfiguration);
|
||||
Close(_profileConfiguration);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.DryIoc.Factories;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.Builders;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using Newtonsoft.Json;
|
||||
@@ -24,23 +24,19 @@ namespace Artemis.UI.Screens.Sidebar;
|
||||
public class SidebarCategoryViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly ISidebarVmFactory _vmFactory;
|
||||
private readonly IWindowService _windowService;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly ISidebarVmFactory _vmFactory;
|
||||
private readonly IRouter _router;
|
||||
private ObservableAsPropertyHelper<bool>? _isCollapsed;
|
||||
private ObservableAsPropertyHelper<bool>? _isSuspended;
|
||||
private SidebarProfileConfigurationViewModel? _selectedProfileConfiguration;
|
||||
|
||||
public SidebarCategoryViewModel(ProfileCategory profileCategory,
|
||||
IProfileService profileService,
|
||||
IWindowService windowService,
|
||||
IProfileEditorService profileEditorService,
|
||||
ISidebarVmFactory vmFactory)
|
||||
public SidebarCategoryViewModel(ProfileCategory profileCategory, IProfileService profileService, IWindowService windowService, ISidebarVmFactory vmFactory, IRouter router)
|
||||
{
|
||||
_profileService = profileService;
|
||||
_windowService = windowService;
|
||||
_profileEditorService = profileEditorService;
|
||||
_vmFactory = vmFactory;
|
||||
_router = router;
|
||||
|
||||
ProfileCategory = profileCategory;
|
||||
SourceList<ProfileConfiguration> profileConfigurations = new();
|
||||
@@ -66,6 +62,14 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase
|
||||
|
||||
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
|
||||
Observable.FromEventPattern<ProfileConfigurationEventArgs>(x => profileCategory.ProfileConfigurationAdded += x, x => profileCategory.ProfileConfigurationAdded -= x)
|
||||
.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)
|
||||
.Subscribe(e => profileConfigurations.RemoveMany(profileConfigurations.Items.Where(c => c == e.EventArgs.ProfileConfiguration)))
|
||||
.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);
|
||||
_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 =>
|
||||
@@ -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 (ProfileCategory.ProfileConfigurations.Any(c => c.IsBeingEdited))
|
||||
_profileEditorService.ChangeCurrentProfileConfiguration(null);
|
||||
await _router.Navigate("home");
|
||||
_profileService.DeleteProfileCategory(ProfileCategory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Newtonsoft.Json;
|
||||
@@ -17,15 +18,15 @@ namespace Artemis.UI.Screens.Sidebar;
|
||||
|
||||
public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly IRouter _router;
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly IWindowService _windowService;
|
||||
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;
|
||||
_profileEditorService = profileEditorService;
|
||||
_windowService = windowService;
|
||||
|
||||
ProfileConfiguration = profileConfiguration;
|
||||
@@ -98,7 +99,7 @@ public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase
|
||||
return;
|
||||
|
||||
if (ProfileConfiguration.IsBeingEdited)
|
||||
_profileEditorService.ChangeCurrentProfileConfiguration(null);
|
||||
await _router.Navigate("home");
|
||||
_profileService.RemoveProfileConfiguration(ProfileConfiguration);
|
||||
}
|
||||
|
||||
@@ -131,4 +132,9 @@ public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase
|
||||
ProfileConfigurationExportModel export = _profileService.ExportProfile(ProfileConfiguration);
|
||||
_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 Artemis.UI.Shared;
|
||||
using DryIoc;
|
||||
using Material.Icons;
|
||||
using ReactiveUI;
|
||||
|
||||
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 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)
|
||||
public SidebarScreenViewModel(MaterialIconKind icon, string displayName, string path)
|
||||
{
|
||||
Icon = icon;
|
||||
Path = path;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
|
||||
public MaterialIconKind Icon { get; }
|
||||
public string Path { get; }
|
||||
|
||||
public abstract Type ScreenType { get; }
|
||||
public abstract MainScreenViewModel CreateInstance(IContainer container, IScreen screen);
|
||||
public bool Matches(string? path)
|
||||
{
|
||||
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.Services;
|
||||
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.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.Builders;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Avalonia.ReactiveUI;
|
||||
using DryIoc;
|
||||
using Avalonia.Threading;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using Material.Icons;
|
||||
@@ -28,50 +23,34 @@ namespace Artemis.UI.Screens.Sidebar;
|
||||
|
||||
public class SidebarViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly IScreen _hostScreen;
|
||||
private readonly IContainer _container;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly IProfileEditorVmFactory _profileEditorVmFactory;
|
||||
private readonly IRouter _router;
|
||||
private readonly IWindowService _windowService;
|
||||
private SidebarScreenViewModel? _selectedSidebarScreen;
|
||||
private ReadOnlyObservableCollection<SidebarCategoryViewModel> _sidebarCategories = new(new ObservableCollection<SidebarCategoryViewModel>());
|
||||
|
||||
public SidebarViewModel(IScreen hostScreen,
|
||||
IContainer container,
|
||||
IProfileService profileService,
|
||||
IWindowService windowService,
|
||||
IProfileEditorService profileEditorService,
|
||||
ISidebarVmFactory sidebarVmFactory,
|
||||
IProfileEditorVmFactory profileEditorVmFactory)
|
||||
public SidebarViewModel(IRouter router, IProfileService profileService, IWindowService windowService, ISidebarVmFactory sidebarVmFactory)
|
||||
{
|
||||
_hostScreen = hostScreen;
|
||||
_container = container;
|
||||
_router = router;
|
||||
_windowService = windowService;
|
||||
_profileEditorService = profileEditorService;
|
||||
_profileEditorVmFactory = profileEditorVmFactory;
|
||||
|
||||
SidebarScreens = new ObservableCollection<SidebarScreenViewModel>
|
||||
{
|
||||
new SidebarScreenViewModel<HomeViewModel>(MaterialIconKind.Home, "Home"),
|
||||
new(MaterialIconKind.Home, "Home", "home"),
|
||||
#if DEBUG
|
||||
new SidebarScreenViewModel<WorkshopViewModel>(MaterialIconKind.TestTube, "Workshop"),
|
||||
new(MaterialIconKind.TestTube, "Workshop", "workshop"),
|
||||
#endif
|
||||
new SidebarScreenViewModel<SurfaceEditorViewModel>(MaterialIconKind.Devices, "Surface Editor"),
|
||||
new SidebarScreenViewModel<SettingsViewModel>(MaterialIconKind.Cog, "Settings")
|
||||
new(MaterialIconKind.Devices, "Surface Editor", "surface-editor"),
|
||||
new(MaterialIconKind.Cog, "Settings", "settings")
|
||||
};
|
||||
|
||||
AddCategory = ReactiveCommand.CreateFromTask(ExecuteAddCategory);
|
||||
|
||||
SourceList<ProfileCategory> profileCategories = new();
|
||||
|
||||
this.WhenAnyValue(vm => vm.SelectedSidebarScreen).WhereNotNull().Subscribe(NavigateToScreen);
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
this.WhenAnyObservable(vm => vm._hostScreen.Router.CurrentViewModel).WhereNotNull()
|
||||
.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);
|
||||
_router.CurrentPath.WhereNotNull().Subscribe(r => SelectedSidebarScreen = SidebarScreens.FirstOrDefault(s => s.Matches(r))).DisposeWith(d);
|
||||
|
||||
Observable.FromEventPattern<ProfileCategoryEventArgs>(x => profileService.ProfileCategoryAdded += x, x => profileService.ProfileCategoryAdded -= x)
|
||||
.Subscribe(e => profileCategories.Add(e.EventArgs.ProfileCategory))
|
||||
@@ -127,21 +106,18 @@ public class SidebarViewModel : ActivatableViewModelBase
|
||||
.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)
|
||||
{
|
||||
// If the current screen changed through external means and already matches, don't navigate again
|
||||
if (_hostScreen.Router.GetCurrentViewModel()?.GetType() == sidebarScreenViewModel.ScreenType)
|
||||
return;
|
||||
|
||||
_hostScreen.Router.Navigate.Execute(sidebarScreenViewModel.CreateInstance(_container, _hostScreen));
|
||||
_profileEditorService.ChangeCurrentProfileConfiguration(null);
|
||||
Dispatcher.UIThread.Invoke(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _router.Navigate(sidebarScreenViewModel.Path, new RouterNavigationOptions {IgnoreOnPartialMatch = true});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_windowService.ShowExceptionDialog("Navigation failed", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.DryIoc.Factories;
|
||||
using Artemis.UI.Extensions;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Avalonia;
|
||||
using ReactiveUI;
|
||||
@@ -16,7 +17,7 @@ using SkiaSharp;
|
||||
|
||||
namespace Artemis.UI.Screens.SurfaceEditor;
|
||||
|
||||
public class SurfaceEditorViewModel : MainScreenViewModel
|
||||
public class SurfaceEditorViewModel : ActivatableViewModelBase, IMainScreenViewModel
|
||||
{
|
||||
private readonly IDeviceService _deviceService;
|
||||
private readonly IDeviceVmFactory _deviceVmFactory;
|
||||
@@ -30,14 +31,13 @@ public class SurfaceEditorViewModel : MainScreenViewModel
|
||||
private double _overlayOpacity;
|
||||
private bool _saving;
|
||||
|
||||
public SurfaceEditorViewModel(IScreen hostScreen,
|
||||
ICoreService coreService,
|
||||
public SurfaceEditorViewModel(ICoreService coreService,
|
||||
IRgbService rgbService,
|
||||
ISurfaceVmFactory surfaceVmFactory,
|
||||
ISettingsService settingsService,
|
||||
IDeviceVmFactory deviceVmFactory,
|
||||
IWindowService windowService,
|
||||
IDeviceService deviceService) : base(hostScreen, "surface-editor")
|
||||
IDeviceService deviceService)
|
||||
{
|
||||
_rgbService = rgbService;
|
||||
_surfaceVmFactory = surfaceVmFactory;
|
||||
@@ -71,6 +71,8 @@ public class SurfaceEditorViewModel : MainScreenViewModel
|
||||
}).DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public ViewModelBase? TitleBarViewModel => null;
|
||||
|
||||
public bool ColorDevices
|
||||
{
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
<StackPanel Margin="12" Spacing="5">
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="5">
|
||||
<TextBlock Classes="h4">Navigation test</TextBlock>
|
||||
<TextBox Text="{CompiledBinding NavigationPath}" Watermark="Enter a navigation path"></TextBox>
|
||||
<Button Command="{CompiledBinding TestNavigation}">
|
||||
Navigate
|
||||
</Button>
|
||||
|
||||
<TextBlock Classes="h4">Notification tests</TextBlock>
|
||||
<Button Command="{CompiledBinding ShowNotification}" CommandParameter="{x:Static builders:NotificationSeverity.Informational}">
|
||||
Notification test (default)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.Builders;
|
||||
using Avalonia.Input;
|
||||
@@ -9,7 +11,7 @@ using SkiaSharp;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop;
|
||||
|
||||
public class WorkshopViewModel : MainScreenViewModel
|
||||
public class WorkshopViewModel : ActivatableViewModelBase, IMainScreenViewModel
|
||||
{
|
||||
private readonly ObservableAsPropertyHelper<Cursor> _cursor;
|
||||
private readonly INotificationService _notificationService;
|
||||
@@ -26,17 +28,21 @@ public class WorkshopViewModel : MainScreenViewModel
|
||||
|
||||
private StandardCursorType _selectedCursor;
|
||||
private double _testValue;
|
||||
private string? _navigationPath;
|
||||
|
||||
public WorkshopViewModel(IScreen hostScreen, INotificationService notificationService) : base(hostScreen, "workshop")
|
||||
public WorkshopViewModel(INotificationService notificationService, IRouter router)
|
||||
{
|
||||
_notificationService = notificationService;
|
||||
_cursor = this.WhenAnyValue(vm => vm.SelectedCursor).Select(c => new Cursor(c)).ToProperty(this, vm => vm.Cursor);
|
||||
|
||||
DisplayName = "Workshop";
|
||||
ShowNotification = ReactiveCommand.Create<NotificationSeverity>(ExecuteShowNotification);
|
||||
TestNavigation = ReactiveCommand.CreateFromTask(async () => await router.Navigate(NavigationPath!), this.WhenAnyValue(vm => vm.NavigationPath).Select(p => !string.IsNullOrWhiteSpace(p)));
|
||||
}
|
||||
|
||||
public ViewModelBase? TitleBarViewModel => null;
|
||||
public ReactiveCommand<NotificationSeverity, Unit> ShowNotification { get; set; }
|
||||
public ReactiveCommand<Unit, Unit> TestNavigation { get; set; }
|
||||
|
||||
public StandardCursorType SelectedCursor
|
||||
{
|
||||
@@ -46,6 +52,12 @@ public class WorkshopViewModel : MainScreenViewModel
|
||||
|
||||
public Cursor Cursor => _cursor.Value;
|
||||
|
||||
public string? NavigationPath
|
||||
{
|
||||
get => _navigationPath;
|
||||
set => RaiseAndSetIfChanged(ref _navigationPath, value);
|
||||
}
|
||||
|
||||
public ColorGradient ColorGradient
|
||||
{
|
||||
get => _colorGradient;
|
||||
|
||||
Reference in new issue
Block a user