Enhances setup wizard workflow

Refactors the setup wizard to streamline the initial user
experience. Includes the following changes:

- Adds nyan cat animation during feature installations for user enjoyment
- Skips essential feature selection if workshop is unreachable
- Moves directly to settings after device/surface selection
- Adds option to configure settings, after device configuration
This commit is contained in:
Robert committed 2025-12-04 23:09:28 +01:00
1 parent 6988255dce
commit 13c497f41e
10 files changed
+71 -23

No files matched your search

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls;
@@ -8,6 +8,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
using DryIoc;
using FluentAvalonia.UI.Controls;
using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton;
namespace Artemis.UI.Shared.Services;
@@ -94,6 +95,7 @@ internal class WindowService : IWindowService
.WithTitle(title)
.WithContent(message)
.HavingPrimaryButton(b => b.WithText(confirm))
.WithDefaultButton(ContentDialogButton.Primary)
.WithCloseButtonText(cancel)
.ShowAsync();
+1 -4
View File
@@ -34,10 +34,7 @@ internal class Program
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
return AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
return AppBuilder.Configure<App>().UsePlatformDetect().LogToTrace().UseReactiveUI();
}
public static void CreateLogger(IContainer container)
@@ -0,0 +1 @@
{"v":"5.5.4","fr":60,"ip":0,"op":40,"w":800,"h":800,"nm":"nyan cat","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":32,"s":[100]},{"t":37,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[637.938,650.813,0],"to":[-16.667,0,0],"ti":[16.667,0,0]},{"t":38,"s":[537.938,650.813,0]}],"ix":2},"a":{"a":0,"k":[303.75,98.813,0],"ix":1},"s":{"a":0,"k":[93.465,93.465,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":27,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":32,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":37,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":270,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":27,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":32,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":37,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":180,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 3","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":27,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":32,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":37,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":27,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":32,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":37,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBLine truncated
@@ -54,8 +54,8 @@
<DockPanel Grid.Row="2" LastChildFill="False" IsVisible="{CompiledBinding !Screen.HideAllButtons}" HorizontalSpacing="10">
<Button Command="{CompiledBinding SkipOrFinishWizard}" IsVisible="{CompiledBinding !Screen.ShowFinish}" DockPanel.Dock="Left">Skip &amp; close</Button>
<Button Command="{CompiledBinding Screen.Continue}" IsVisible="{CompiledBinding !Screen.ShowFinish}" Width="80" DockPanel.Dock="Right">Continue</Button>
<Button Command="{CompiledBinding Screen.GoBack}" IsEnabled="{CompiledBinding Screen.ShowGoBack}" DockPanel.Dock="Right">Back</Button>
<Button Command="{CompiledBinding SkipOrFinishWizard}" IsVisible="{CompiledBinding Screen.ShowFinish}" Width="80" DockPanel.Dock="Right">Finish</Button>
<Button Command="{CompiledBinding Screen.GoBack}" IsEnabled="{CompiledBinding Screen.ShowGoBack}" DockPanel.Dock="Right">Back</Button>
</DockPanel>
</Grid>
@@ -9,7 +9,7 @@
<Panel>
<!-- Selection stage -->
<Grid RowDefinitions="Auto,*,Auto" IsVisible="{CompiledBinding CurrentEntry, Converter={x:Static ObjectConverters.IsNull}}">
<TextBlock Grid.Row="0" TextWrapping="Wrap">
<TextBlock Grid.Row="0" TextWrapping="Wrap" IsVisible="{CompiledBinding !FetchingDefaultEntries}">
Below is a list of default features that can be installed to get you started with Artemis. You can always install or uninstall features later via the Workshop.
</TextBlock>
@@ -56,14 +56,23 @@
</Grid>
<!-- Installation stage -->
<StackPanel IsVisible="{CompiledBinding CurrentEntry, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock Classes="card-title">
Currently installing
</TextBlock>
<ContentControl Content="{CompiledBinding CurrentEntry}" />
<StackPanel Width="400" Margin="100 0 0 0" IsVisible="{CompiledBinding CurrentEntry, Converter={x:Static ObjectConverters.IsNotNull}}">
<Lottie Path="/Assets/Animations/nyan.json" RepeatCount="-1" Width="300" Height="300"></Lottie>
<ProgressBar ShowProgressText="True" ProgressTextFormat="{}{0}/{3} Features Installed ({1:0}%)" Minimum="0" Maximum="{CompiledBinding TotalEntries}"
Value="{CompiledBinding InstalledEntries}" />
<ProgressBar Minimum="0"
Maximum="{CompiledBinding TotalEntries}"
Value="{CompiledBinding CurrentEntry.InstallProgress, FallbackValue=0}"
IsIndeterminate="{CompiledBinding CurrentEntry.Enabling, FallbackValue=true}"
Margin="0 0 0 5" />
<TextBlock>
<Run>Currently installing: </Run>
<Run Text="{CompiledBinding CurrentEntry.Entry.Name}" FontWeight="Bold" />
</TextBlock>
<ProgressBar Minimum="0" Maximum="{CompiledBinding TotalEntries}" Value="{CompiledBinding InstalledEntries}" Margin="0 20 0 5" />
<TextBlock>
<Run Text="{CompiledBinding RemainingEntries}" FontWeight="Bold" />
<Run>feature(s) remaining, hold on tight!</Run>
</TextBlock>
</StackPanel>
</Panel>
</UserControl>
@@ -27,10 +27,12 @@ public partial class DefaultEntriesStepViewModel : WizardStepViewModel
private readonly IDeviceService _deviceService;
private readonly IWorkshopClient _client;
private readonly Func<IEntrySummary, DefaultEntryItemViewModel> _getDefaultEntryItemViewModel;
private readonly ObservableAsPropertyHelper<int> _remainingEntries;
public ObservableCollection<DefaultEntryItemViewModel> DeviceProviderEntryViewModels { get; } = [];
public ObservableCollection<DefaultEntryItemViewModel> EssentialEntryViewModels { get; } = [];
public ObservableCollection<DefaultEntryItemViewModel> OtherEntryViewModels { get; } = [];
public int RemainingEntries => _remainingEntries.Value;
public DefaultEntriesStepViewModel(IWorkshopService workshopService, IDeviceService deviceService, IWorkshopClient client,
Func<IEntrySummary, DefaultEntryItemViewModel> getDefaultEntryItemViewModel)
@@ -38,6 +40,9 @@ public partial class DefaultEntriesStepViewModel : WizardStepViewModel
_deviceService = deviceService;
_client = client;
_getDefaultEntryItemViewModel = getDefaultEntryItemViewModel;
_remainingEntries = this.WhenAnyValue(vm => vm.InstalledEntries, vm => vm.TotalEntries)
.Select(t => t.Item2 - t.Item1)
.ToProperty(this, vm => vm.RemainingEntries);
ContinueText = "Install selected entries";
Continue = ReactiveCommand.CreateFromTask(async ct =>
@@ -16,12 +16,13 @@ using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using StrawberryShake;
using Serilog;
namespace Artemis.UI.Screens.StartupWizard.Steps;
public partial class DefaultEntryItemViewModel : ActivatableViewModelBase
{
private readonly ILogger _logger;
private readonly IWorkshopService _workshopService;
private readonly IWindowService _windowService;
private readonly IPluginManagementService _pluginManagementService;
@@ -30,15 +31,20 @@ public partial class DefaultEntryItemViewModel : ActivatableViewModelBase
[Notify] private bool _isInstalled;
[Notify] private bool _shouldInstall;
[Notify] private bool _enabling;
[Notify] private float _installProgress;
public DefaultEntryItemViewModel(IEntrySummary entry, IWorkshopService workshopService, IWindowService windowService, IPluginManagementService pluginManagementService, ISettingsVmFactory settingsVmFactory)
public DefaultEntryItemViewModel(ILogger logger, IEntrySummary entry, IWorkshopService workshopService, IWindowService windowService, IPluginManagementService pluginManagementService,
ISettingsVmFactory settingsVmFactory)
{
_logger = logger;
_workshopService = workshopService;
_windowService = windowService;
_pluginManagementService = pluginManagementService;
_settingsVmFactory = settingsVmFactory;
Entry = entry;
_progress.ProgressChanged += (_, f) => InstallProgress = f.ProgressPercentage;
this.WhenActivated((CompositeDisposable _) => { IsInstalled = workshopService.GetInstalledEntry(entry.Id) != null; });
}
@@ -61,16 +67,18 @@ public partial class DefaultEntryItemViewModel : ActivatableViewModelBase
.WithCloseButtonText("Skip and continue")
.ShowAsync();
}
// If the entry is a plugin, enable the plugin
// If the entry is a plugin, enable the plugin and all features
else if (result.Entry?.EntryType == EntryType.Plugin)
{
await EnablePlugin(result.Entry);
Enabling = true;
await EnablePluginAndFeatures(result.Entry);
Enabling = false;
}
return result.IsSuccess;
}
private async Task EnablePlugin(InstalledEntry entry)
private async Task EnablePluginAndFeatures(InstalledEntry entry)
{
if (!entry.TryGetMetadata("PluginId", out Guid pluginId))
throw new InvalidOperationException("Plugin entry does not contain a PluginId metadata value.");
@@ -82,5 +90,21 @@ public partial class DefaultEntryItemViewModel : ActivatableViewModelBase
// There's quite a bit of UI involved in enabling a plugin, borrowing the PluginSettingsViewModel for this
PluginViewModel pluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { }));
await pluginViewModel.UpdateEnabled(true);
// Find features without prerequisites to enable
foreach (PluginFeatureInfo pluginFeatureInfo in plugin.Features)
{
if (pluginFeatureInfo.Instance == null || pluginFeatureInfo.Instance.IsEnabled || pluginFeatureInfo.Prerequisites.Count != 0)
continue;
try
{
_pluginManagementService.EnablePluginFeature(pluginFeatureInfo.Instance, true);
}
catch (Exception e)
{
_logger.Warning(e, "Failed to enable plugin feature '{FeatureName}', skipping", pluginFeatureInfo.Name);
}
}
}
}
@@ -19,7 +19,7 @@ public class SurfaceStepViewModel : WizardStepViewModel
_deviceService = deviceService;
SelectLayout = ReactiveCommand.Create<string>(ExecuteSelectLayout);
Continue = ReactiveCommand.Create(() => Wizard.ChangeScreen<DefaultEntriesStepViewModel>());
Continue = ReactiveCommand.Create(() => Wizard.ChangeScreen<SettingsStepViewModel>());
GoBack = ReactiveCommand.Create(() => Wizard.ChangeScreen<LayoutsStepViewModel>());
}
@@ -30,6 +30,6 @@ public class SurfaceStepViewModel : WizardStepViewModel
// TODO: Implement the layout
_deviceService.AutoArrangeDevices();
Wizard.ChangeScreen<DefaultEntriesStepViewModel>();
Wizard.ChangeScreen<SettingsStepViewModel>();
}
}
@@ -1,9 +1,15 @@
namespace Artemis.UI.Screens.StartupWizard.Steps;
using Artemis.Core;
using Artemis.Core.Services;
namespace Artemis.UI.Screens.StartupWizard.Steps;
public class WorkshopUnreachableStepViewModel : WizardStepViewModel
{
public WorkshopUnreachableStepViewModel()
private readonly ISettingsService _settingsService;
public WorkshopUnreachableStepViewModel(ISettingsService settingsService)
{
_settingsService = settingsService;
HideAllButtons = true;
}
@@ -14,6 +20,10 @@ public class WorkshopUnreachableStepViewModel : WizardStepViewModel
public void Skip()
{
PluginSetting<bool> setting = _settingsService.GetSetting("UI.SetupWizardCompleted", false);
setting.Value = false;
setting.Save();
Wizard.Close(false);
}
}