mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 11:56:30 +00:00
Add AXAML hot reload (debug only)
Major progress on default entries too I guess lmao
This commit is contained in:
1 parent
20fb6b7662
commit
b351f685f7
24 files changed
+963
-682
No files matched your search
@@ -7,26 +7,52 @@
|
||||
x:Class="Artemis.UI.Screens.StartupWizard.Steps.DefaultEntriesStepView"
|
||||
x:DataType="steps:DefaultEntriesStepViewModel">
|
||||
<Border Classes="card">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
<StackPanel Grid.Row="0">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
Installing default plugins and profiles, these will provide you with a basic setup to get started with Artemis.
|
||||
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>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="2">
|
||||
<Run Text="Installed"/>
|
||||
<Run Text="{CompiledBinding InstalledEntries}"/>
|
||||
<Run Text=" of "/>
|
||||
<Run Text="{CompiledBinding TotalEntries}"/>
|
||||
<Run Text=" entries."/>
|
||||
</TextBlock>
|
||||
<TextBlock Classes="card-title" IsVisible="{CompiledBinding DeviceProviderEntryViewModels.Count}">
|
||||
Device providers
|
||||
</TextBlock>
|
||||
<ItemsControl ItemsSource="{CompiledBinding DeviceProviderEntryViewModels}" Margin="0,0,5,0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="2" ColumnSpacing="5" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
|
||||
<ProgressBar Grid.Row="3" Minimum="0" Maximum="{CompiledBinding TotalEntries}" IsIndeterminate="{CompiledBinding FetchingDefaultEntries}" Value="{CompiledBinding InstalledEntries}" />
|
||||
<StackPanel Grid.Row="4" Orientation="Vertical" IsVisible="{CompiledBinding CurrentEntry, Converter={x:Static ObjectConverters.IsNotNull}}" Margin="0 10">
|
||||
<TextBlock Text="{CompiledBinding CurrentEntry.Name, StringFormat='Currently installing: {0}'}" />
|
||||
<ProgressBar Minimum="0" Maximum="100" Value="{CompiledBinding InstallProgress}" />
|
||||
<TextBlock Classes="card-title" IsVisible="{CompiledBinding EssentialEntryViewModels.Count}">
|
||||
Essentials
|
||||
</TextBlock>
|
||||
<ItemsControl ItemsSource="{CompiledBinding EssentialEntryViewModels}" Margin="0,0,5,0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="2" ColumnSpacing="5"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Classes="card-title" IsVisible="{CompiledBinding OtherEntryViewModels.Count}">
|
||||
Other features
|
||||
</TextBlock>
|
||||
<ItemsControl ItemsSource="{CompiledBinding OtherEntryViewModels}" Margin="0,0,5,0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="2" ColumnSpacing="5"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ProgressBar Grid.Row="1"
|
||||
Minimum="0"
|
||||
Maximum="{CompiledBinding TotalEntries}"
|
||||
IsIndeterminate="{CompiledBinding FetchingDefaultEntries}"
|
||||
Value="{CompiledBinding CurrentEntries}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -8,7 +9,6 @@ using Artemis.UI.Extensions;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Utilities;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
||||
using Artemis.WebClient.Workshop.Services;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using ReactiveUI;
|
||||
@@ -20,25 +20,31 @@ public partial class DefaultEntriesStepViewModel : WizardStepViewModel
|
||||
{
|
||||
[Notify] private bool _workshopReachable;
|
||||
[Notify] private bool _fetchingDefaultEntries;
|
||||
[Notify] private int _totalEntries;
|
||||
[Notify] private int _installedEntries;
|
||||
[Notify] private int _installProgress;
|
||||
[Notify] private IEntrySummary? _currentEntry;
|
||||
[Notify] private bool _installed;
|
||||
[Notify] private int _currentEntries;
|
||||
[Notify] private int _totalEntries = 1;
|
||||
|
||||
private readonly IWorkshopService _workshopService;
|
||||
private readonly IWorkshopClient _client;
|
||||
private readonly IWindowService _windowService;
|
||||
private readonly Progress<StreamProgress> _currentEntryProgress = new();
|
||||
private readonly Func<IEntrySummary, DefaultEntryItemViewModel> _getDefaultEntryItemViewModel;
|
||||
|
||||
public ObservableCollection<DefaultEntryItemViewModel> DeviceProviderEntryViewModels { get; } = [];
|
||||
public ObservableCollection<DefaultEntryItemViewModel> EssentialEntryViewModels { get; } = [];
|
||||
public ObservableCollection<DefaultEntryItemViewModel> OtherEntryViewModels { get; } = [];
|
||||
|
||||
public DefaultEntriesStepViewModel(IWorkshopService workshopService, IDeviceService deviceService, IWorkshopClient client, IWindowService windowService)
|
||||
public DefaultEntriesStepViewModel(IWorkshopService workshopService, IDeviceService deviceService, IWorkshopClient client,
|
||||
Func<IEntrySummary, DefaultEntryItemViewModel> getDefaultEntryItemViewModel)
|
||||
{
|
||||
_workshopService = workshopService;
|
||||
_client = client;
|
||||
_windowService = windowService;
|
||||
_currentEntryProgress.ProgressChanged += (_, f) => InstallProgress = f.ProgressPercentage;
|
||||
_getDefaultEntryItemViewModel = getDefaultEntryItemViewModel;
|
||||
|
||||
Continue = ReactiveCommand.Create(() => Wizard.ChangeScreen<SettingsStepViewModel>());
|
||||
ContinueText = "Install selected entries";
|
||||
Continue = ReactiveCommand.CreateFromTask(async ct =>
|
||||
{
|
||||
if (Installed)
|
||||
Wizard.ChangeScreen<SettingsStepViewModel>();
|
||||
else
|
||||
await Install(ct);
|
||||
});
|
||||
GoBack = ReactiveCommand.Create(() =>
|
||||
{
|
||||
if (deviceService.EnabledDevices.Count == 0)
|
||||
@@ -49,22 +55,57 @@ public partial class DefaultEntriesStepViewModel : WizardStepViewModel
|
||||
|
||||
this.WhenActivatedAsync(async d =>
|
||||
{
|
||||
WorkshopReachable = await _workshopService.ValidateWorkshopStatus(d.AsCancellationToken());
|
||||
WorkshopReachable = await workshopService.ValidateWorkshopStatus(d.AsCancellationToken());
|
||||
if (WorkshopReachable)
|
||||
{
|
||||
await InstallDefaultEntries(d.AsCancellationToken());
|
||||
await GetDefaultEntries(d.AsCancellationToken());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> InstallDefaultEntries(CancellationToken cancellationToken)
|
||||
private async Task Install(CancellationToken cancellationToken)
|
||||
{
|
||||
FetchingDefaultEntries = true;
|
||||
TotalEntries = 0;
|
||||
InstalledEntries = 0;
|
||||
// Remove entries that aren't to be installed
|
||||
RemoveUnselectedEntries(DeviceProviderEntryViewModels);
|
||||
RemoveUnselectedEntries(EssentialEntryViewModels);
|
||||
RemoveUnselectedEntries(OtherEntryViewModels);
|
||||
|
||||
TotalEntries = DeviceProviderEntryViewModels.Count + EssentialEntryViewModels.Count + OtherEntryViewModels.Count;
|
||||
CurrentEntries = 0;
|
||||
|
||||
// Install entries one by one, removing them from the list as we go
|
||||
List<DefaultEntryItemViewModel> entries = [..DeviceProviderEntryViewModels, ..EssentialEntryViewModels, ..OtherEntryViewModels];
|
||||
foreach (DefaultEntryItemViewModel defaultEntryItemViewModel in entries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
bool removeFromList = await defaultEntryItemViewModel.InstallEntry(cancellationToken);
|
||||
if (!removeFromList)
|
||||
break;
|
||||
|
||||
DeviceProviderEntryViewModels.Remove(defaultEntryItemViewModel);
|
||||
EssentialEntryViewModels.Remove(defaultEntryItemViewModel);
|
||||
OtherEntryViewModels.Remove(defaultEntryItemViewModel);
|
||||
CurrentEntries++;
|
||||
}
|
||||
|
||||
Installed = true;
|
||||
ContinueText = "Continue";
|
||||
}
|
||||
|
||||
private void RemoveUnselectedEntries(ObservableCollection<DefaultEntryItemViewModel> entryViewModels)
|
||||
{
|
||||
List<DefaultEntryItemViewModel> toRemove = entryViewModels.Where(e => !e.ShouldInstall).ToList();
|
||||
foreach (DefaultEntryItemViewModel defaultEntryItemViewModel in toRemove)
|
||||
entryViewModels.Remove(defaultEntryItemViewModel);
|
||||
|
||||
}
|
||||
|
||||
private async Task GetDefaultEntries(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!WorkshopReachable)
|
||||
return false;
|
||||
return;
|
||||
|
||||
FetchingDefaultEntries = true;
|
||||
|
||||
IOperationResult<IGetDefaultEntriesResult> result = await _client.GetDefaultEntries.ExecuteAsync(100, null, cancellationToken);
|
||||
List<IEntrySummary> entries = result.Data?.EntriesV2?.Edges?.Select(e => e.Node).Cast<IEntrySummary>().ToList() ?? [];
|
||||
@@ -75,41 +116,25 @@ public partial class DefaultEntriesStepViewModel : WizardStepViewModel
|
||||
entries.AddRange(result.Data.EntriesV2.Edges.Select(e => e.Node));
|
||||
}
|
||||
|
||||
await Task.Delay(1000);
|
||||
FetchingDefaultEntries = false;
|
||||
TotalEntries = entries.Count;
|
||||
|
||||
if (entries.Count == 0)
|
||||
return false;
|
||||
|
||||
DeviceProviderEntryViewModels.Clear();
|
||||
EssentialEntryViewModels.Clear();
|
||||
OtherEntryViewModels.Clear();
|
||||
foreach (IEntrySummary entry in entries)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return false;
|
||||
|
||||
CurrentEntry = entry;
|
||||
|
||||
// Skip entries without a release and entries that are already installed
|
||||
if (entry.LatestRelease == null || _workshopService.GetInstalledEntry(entry.Id) != null)
|
||||
{
|
||||
InstalledEntries++;
|
||||
if (entry.DefaultEntryInfo == null)
|
||||
continue;
|
||||
}
|
||||
|
||||
EntryInstallResult installResult = await _workshopService.InstallEntry(entry, entry.LatestRelease, _currentEntryProgress, cancellationToken);
|
||||
|
||||
// Unlikely as default entries do nothing fancy
|
||||
if (!installResult.IsSuccess)
|
||||
{
|
||||
await _windowService.CreateContentDialog().WithTitle("Failed to install entry")
|
||||
.WithContent($"Failed to install entry '{entry.Name}' ({entry.Id}): {installResult.Message}")
|
||||
.WithCloseButtonText("Skip and continue")
|
||||
.ShowAsync();
|
||||
}
|
||||
|
||||
InstalledEntries++;
|
||||
DefaultEntryItemViewModel viewModel = _getDefaultEntryItemViewModel(entry);
|
||||
viewModel.ShouldInstall = entry.DefaultEntryInfo.IsEssential;
|
||||
|
||||
if (entry.DefaultEntryInfo.IsDeviceProvider)
|
||||
DeviceProviderEntryViewModels.Add(viewModel);
|
||||
else if (entry.DefaultEntryInfo.IsEssential)
|
||||
EssentialEntryViewModels.Add(viewModel);
|
||||
else
|
||||
OtherEntryViewModels.Add(viewModel);
|
||||
}
|
||||
|
||||
return true;
|
||||
FetchingDefaultEntries = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<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:il="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:steps="clr-namespace:Artemis.UI.Screens.StartupWizard.Steps"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.StartupWizard.Steps.DefaultEntryItemView"
|
||||
x:DataType="steps:DefaultEntryItemViewModel">
|
||||
<UserControl.Resources>
|
||||
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
|
||||
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||
</UserControl.Resources>
|
||||
<Border Classes="card" Padding="15" Margin="0 5">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" RowDefinitions="*, Auto">
|
||||
<!-- Icon -->
|
||||
<Border Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
CornerRadius="6"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 10 0"
|
||||
Width="80"
|
||||
Height="80"
|
||||
ClipToBounds="True">
|
||||
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
|
||||
</Border>
|
||||
|
||||
<!-- Body -->
|
||||
<Grid Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal">
|
||||
<TextBlock Classes="h5" Margin="0 0 0 5" TextTrimming="CharacterEllipsis" Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Row="1"
|
||||
Classes="subtitle"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}" />
|
||||
|
||||
<ItemsControl Grid.Row="2" ItemsSource="{CompiledBinding Entry.Categories}" Margin="0 8 0 0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0" />
|
||||
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
|
||||
<!-- Management -->
|
||||
<Border Grid.Column="3" Grid.Row="0" Grid.RowSpan="2" BorderBrush="{DynamicResource ButtonBorderBrush}" BorderThickness="1 0 0 0" Margin="10 0 0 0" Padding="10 0 0 0" IsVisible="{CompiledBinding !IsInstalled}">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<CheckBox MinHeight="26" Margin="0 4 0 0" IsChecked="{CompiledBinding ShouldInstall}">Install</CheckBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Grid.Column="3" Grid.Row="0" Grid.RowSpan="2" BorderBrush="{DynamicResource ButtonBorderBrush}" BorderThickness="1 0 0 0" Margin="10 0 0 0" Padding="10 0 0 0" IsVisible="{CompiledBinding IsInstalled}">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock>Already installed</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.StartupWizard.Steps;
|
||||
|
||||
public partial class DefaultEntryItemView : ReactiveUserControl<StartupWizard.Steps.DefaultEntryItemViewModel>
|
||||
{
|
||||
public DefaultEntryItemView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.DryIoc.Factories;
|
||||
using Artemis.UI.Screens.Plugins;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Utilities;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
||||
using Artemis.WebClient.Workshop.Models;
|
||||
using Artemis.WebClient.Workshop.Services;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using ReactiveUI;
|
||||
using StrawberryShake;
|
||||
|
||||
namespace Artemis.UI.Screens.StartupWizard.Steps;
|
||||
|
||||
public partial class DefaultEntryItemViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly IWorkshopService _workshopService;
|
||||
private readonly IWindowService _windowService;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private readonly ISettingsVmFactory _settingsVmFactory;
|
||||
private readonly Progress<StreamProgress> _progress = new();
|
||||
|
||||
[Notify] private bool _isInstalled;
|
||||
[Notify] private bool _shouldInstall;
|
||||
|
||||
public DefaultEntryItemViewModel(IEntrySummary entry, IWorkshopService workshopService, IWindowService windowService, IPluginManagementService pluginManagementService, ISettingsVmFactory settingsVmFactory)
|
||||
{
|
||||
_workshopService = workshopService;
|
||||
_windowService = windowService;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
_settingsVmFactory = settingsVmFactory;
|
||||
Entry = entry;
|
||||
|
||||
this.WhenActivated((CompositeDisposable _) => { IsInstalled = workshopService.GetInstalledEntry(entry.Id) != null; });
|
||||
}
|
||||
|
||||
public IEntrySummary Entry { get; }
|
||||
|
||||
public async Task<bool> InstallEntry(CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsInstalled || !ShouldInstall || Entry.LatestRelease == null)
|
||||
return true;
|
||||
|
||||
// Most entries install so quick it looks broken without a small delay
|
||||
Task minimumDelay = Task.Delay(100, cancellationToken);
|
||||
EntryInstallResult result = await _workshopService.InstallEntry(Entry, Entry.LatestRelease, _progress, cancellationToken);
|
||||
await minimumDelay;
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
await _windowService.CreateContentDialog().WithTitle("Failed to install entry")
|
||||
.WithContent($"Failed to install entry '{Entry.Name}' ({Entry.Id}): {result.Message}")
|
||||
.WithCloseButtonText("Skip and continue")
|
||||
.ShowAsync();
|
||||
}
|
||||
// If the entry is a plugin, enable the plugin
|
||||
else if (result.Entry?.EntryType == EntryType.Plugin)
|
||||
{
|
||||
await EnablePlugin(result.Entry);
|
||||
}
|
||||
|
||||
return result.IsSuccess;
|
||||
}
|
||||
|
||||
private async Task EnablePlugin(InstalledEntry entry)
|
||||
{
|
||||
if (!entry.TryGetMetadata("PluginId", out Guid pluginId))
|
||||
throw new InvalidOperationException("Plugin entry does not contain a PluginId metadata value.");
|
||||
|
||||
Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
|
||||
if (plugin == null)
|
||||
throw new InvalidOperationException($"Plugin with id '{pluginId}' does not exist.");
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user