diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj
index df21cdfd2..ee57c4987 100644
--- a/src/Artemis.Core/Artemis.Core.csproj
+++ b/src/Artemis.Core/Artemis.Core.csproj
@@ -42,9 +42,9 @@
-
-
-
+
+
+
diff --git a/src/Artemis.Core/DryIoc/ContainerExtensions.cs b/src/Artemis.Core/DryIoc/ContainerExtensions.cs
index 4f418d453..927f606a9 100644
--- a/src/Artemis.Core/DryIoc/ContainerExtensions.cs
+++ b/src/Artemis.Core/DryIoc/ContainerExtensions.cs
@@ -13,7 +13,7 @@ namespace Artemis.Core.DryIoc;
///
/// Provides an extension method to register services onto a DryIoc .
///
-public static class CoreContainerExtensions
+public static class ContainerExtensions
{
///
/// Registers core services into the container.
diff --git a/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs
index dfcbfbfe7..cb5852eaa 100644
--- a/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs
+++ b/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs
@@ -9,4 +9,6 @@ public interface IQueuedActionRepository : IRepository
void Remove(QueuedActionEntity queuedActionEntity);
List GetAll();
List GetByType(string type);
+ bool IsTypeQueued(string type);
+ void ClearByType(string type);
}
\ No newline at end of file
diff --git a/src/Artemis.Storage/Repositories/QueuedActionRepository.cs b/src/Artemis.Storage/Repositories/QueuedActionRepository.cs
index faa6a304f..f5c83cd0e 100644
--- a/src/Artemis.Storage/Repositories/QueuedActionRepository.cs
+++ b/src/Artemis.Storage/Repositories/QueuedActionRepository.cs
@@ -41,5 +41,17 @@ public class QueuedActionRepository : IQueuedActionRepository
return _repository.Query().Where(q => q.Type == type).ToList();
}
+ ///
+ public bool IsTypeQueued(string type)
+ {
+ return _repository.Query().Where(q => q.Type == type).Count() > 0;
+ }
+
+ ///
+ public void ClearByType(string type)
+ {
+ _repository.DeleteMany(q => q.Type == type);
+ }
+
#endregion
}
\ No newline at end of file
diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj
index 09c2d5479..c8f440c29 100644
--- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj
+++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj
@@ -20,7 +20,7 @@
-
+
diff --git a/src/Artemis.UI.Shared/Converters/BytesToStringConverter.cs b/src/Artemis.UI.Shared/Converters/BytesToStringConverter.cs
new file mode 100644
index 000000000..f6f7da7e6
--- /dev/null
+++ b/src/Artemis.UI.Shared/Converters/BytesToStringConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Humanizer;
+using Humanizer.Bytes;
+
+namespace Artemis.UI.Shared.Converters;
+
+///
+/// Converts bytes to a string
+///
+public class BytesToStringConverter : IValueConverter
+{
+ ///
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is int intBytes)
+ return intBytes.Bytes().Humanize();
+ if (value is long longBytes)
+ return longBytes.Bytes().Humanize();
+ if (value is double doubleBytes)
+ return doubleBytes.Bytes().Humanize();
+
+ return value;
+ }
+
+ ///
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is string formatted && ByteSize.TryParse(formatted, out ByteSize result))
+ return result.Bytes;
+ return value;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI.Shared/DryIoc/ContainerExtensions.cs b/src/Artemis.UI.Shared/DryIoc/ContainerExtensions.cs
index cb7baef36..4c4711b02 100644
--- a/src/Artemis.UI.Shared/DryIoc/ContainerExtensions.cs
+++ b/src/Artemis.UI.Shared/DryIoc/ContainerExtensions.cs
@@ -7,7 +7,7 @@ namespace Artemis.UI.Shared.DryIoc;
///
/// Provides an extension method to register services onto a DryIoc .
///
-public static class UIContainerExtensions
+public static class ContainerExtensions
{
///
/// Registers shared UI services into the container.
diff --git a/src/Artemis.UI.Shared/Services/MainWindow/IMainWindowService.cs b/src/Artemis.UI.Shared/Services/MainWindow/IMainWindowService.cs
index bf7716f0b..e42d644dc 100644
--- a/src/Artemis.UI.Shared/Services/MainWindow/IMainWindowService.cs
+++ b/src/Artemis.UI.Shared/Services/MainWindow/IMainWindowService.cs
@@ -1,4 +1,5 @@
using System;
+using ReactiveUI;
namespace Artemis.UI.Shared.Services.MainWindow;
@@ -12,6 +13,11 @@ public interface IMainWindowService : IArtemisSharedUIService
///
bool IsMainWindowOpen { get; }
+ ///
+ /// Gets or sets the host screen contained in the main window
+ ///
+ IScreen? HostScreen { get; set; }
+
///
/// Sets up the main window provider that controls the state of the main window
///
diff --git a/src/Artemis.UI.Shared/Services/MainWindow/MainWindowService.cs b/src/Artemis.UI.Shared/Services/MainWindow/MainWindowService.cs
index fbdda52e1..98a6cba19 100644
--- a/src/Artemis.UI.Shared/Services/MainWindow/MainWindowService.cs
+++ b/src/Artemis.UI.Shared/Services/MainWindow/MainWindowService.cs
@@ -1,4 +1,5 @@
using System;
+using ReactiveUI;
namespace Artemis.UI.Shared.Services.MainWindow;
@@ -6,6 +7,12 @@ internal class MainWindowService : IMainWindowService
{
private IMainWindowProvider? _mainWindowManager;
+ ///
+ public bool IsMainWindowOpen { get; private set; }
+
+ ///
+ public IScreen? HostScreen { get; set; }
+
protected virtual void OnMainWindowOpened()
{
MainWindowOpened?.Invoke(this, EventArgs.Empty);
@@ -64,8 +71,6 @@ internal class MainWindowService : IMainWindowService
OnMainWindowUnfocused();
}
- public bool IsMainWindowOpen { get; private set; }
-
public void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider)
{
if (mainWindowProvider == null) throw new ArgumentNullException(nameof(mainWindowProvider));
diff --git a/src/Artemis.UI.Shared/Services/NodeEditor/Commands/DuplicateNode.cs b/src/Artemis.UI.Shared/Services/NodeEditor/Commands/DuplicateNode.cs
index 8c60677b6..62340d3a6 100644
--- a/src/Artemis.UI.Shared/Services/NodeEditor/Commands/DuplicateNode.cs
+++ b/src/Artemis.UI.Shared/Services/NodeEditor/Commands/DuplicateNode.cs
@@ -56,7 +56,7 @@ public class DuplicateNode : INodeEditorCommand, IDisposable
if (targetCollection == null)
continue;
while (targetCollection.Count() < sourceCollection.Count())
- targetCollection.CreatePin();
+ targetCollection.Add(targetCollection.CreatePin());
}
// Copy the storage
diff --git a/src/Artemis.UI.Shared/Styles/Artemis.axaml b/src/Artemis.UI.Shared/Styles/Artemis.axaml
index 97dafc8a1..7b838cdbf 100644
--- a/src/Artemis.UI.Shared/Styles/Artemis.axaml
+++ b/src/Artemis.UI.Shared/Styles/Artemis.axaml
@@ -21,6 +21,7 @@
+
diff --git a/src/Artemis.UI.Shared/Styles/Skeleton.axaml b/src/Artemis.UI.Shared/Styles/Skeleton.axaml
new file mode 100644
index 000000000..2596c3fe6
--- /dev/null
+++ b/src/Artemis.UI.Shared/Styles/Skeleton.axaml
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+ This is heading 1
+ This is heading 2
+ This is heading 3
+ This is heading 4
+ This is heading 5
+ This is heading 6
+ This is regular text
+ This is regular text
+ This is regular text
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is heading 1
+ This is heading 2
+ This is heading 3
+ This is heading 4
+ This is heading 5
+ This is heading 6
+ This is regular text
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is heading 1
+ This is heading 2
+ This is heading 3
+ This is heading 4
+ This is heading 5
+ This is heading 6
+ This is regular text
+
+
+
+
+
+
+ 8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs
index 2eb3ce47f..1a7d8147e 100644
--- a/src/Artemis.UI.Windows/ApplicationStateManager.cs
+++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs
@@ -108,8 +108,6 @@ public class ApplicationStateManager
ProcessStartInfo info = new()
{
Arguments = $"-File {script} {source} {destination} {args}",
- WindowStyle = ProcessWindowStyle.Hidden,
- CreateNoWindow = true,
FileName = "PowerShell.exe"
};
Process.Start(info);
diff --git a/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs b/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs
index 4b3bf7fda..337e21744 100644
--- a/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs
+++ b/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs
@@ -1,5 +1,6 @@
using Artemis.Core.Providers;
using Artemis.Core.Services;
+using Artemis.UI.Services.Updating;
using Artemis.UI.Shared.Providers;
using Artemis.UI.Windows.Providers;
using Artemis.UI.Windows.Providers.Input;
@@ -22,5 +23,6 @@ public static class UIContainerExtensions
container.Register(Reuse.Singleton);
container.Register();
container.Register(serviceKey: WindowsInputProvider.Id);
+ container.Register();
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs
new file mode 100644
index 000000000..a73bc2f42
--- /dev/null
+++ b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs
@@ -0,0 +1,165 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Windows.UI.Notifications;
+using Artemis.UI.Screens.Settings;
+using Artemis.UI.Services.Updating;
+using Artemis.UI.Shared.Services.MainWindow;
+using Avalonia.Threading;
+using Microsoft.Toolkit.Uwp.Notifications;
+using ReactiveUI;
+
+namespace Artemis.UI.Windows.Providers;
+
+public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
+{
+ private readonly Func _getReleaseInstaller;
+ private readonly Func _getSettingsViewModel;
+ private readonly IMainWindowService _mainWindowService;
+ private readonly IUpdateService _updateService;
+ private CancellationTokenSource? _cancellationTokenSource;
+
+ public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService,
+ IUpdateService updateService,
+ Func getSettingsViewModel,
+ Func getReleaseInstaller)
+ {
+ _mainWindowService = mainWindowService;
+ _updateService = updateService;
+ _getSettingsViewModel = getSettingsViewModel;
+ _getReleaseInstaller = getReleaseInstaller;
+ ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated;
+ }
+
+ private async void ToastNotificationManagerCompatOnOnActivated(ToastNotificationActivatedEventArgsCompat e)
+ {
+ ToastArguments args = ToastArguments.Parse(e.Argument);
+ string releaseId = args.Get("releaseId");
+ string releaseVersion = args.Get("releaseVersion");
+ string action = "view-changes";
+ if (args.Contains("action"))
+ action = args.Get("action");
+
+ if (action == "install")
+ await InstallRelease(releaseId, releaseVersion);
+ else if (action == "view-changes")
+ ViewRelease(releaseId);
+ else if (action == "cancel")
+ _cancellationTokenSource?.Cancel();
+ else if (action == "restart-for-update")
+ _updateService.RestartForUpdate(false);
+ }
+
+ public void ShowNotification(string releaseId, string releaseVersion)
+ {
+ GetBuilderForRelease(releaseId, releaseVersion)
+ .AddText("Update available")
+ .AddText($"Artemis version {releaseVersion} has been released")
+ .AddButton(new ToastButton()
+ .SetContent("Install")
+ .AddArgument("action", "install").SetAfterActivationBehavior(ToastAfterActivationBehavior.PendingUpdate))
+ .AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-changes"))
+ .Show(t => t.Tag = releaseId);
+ }
+
+ private void ViewRelease(string releaseId)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ _mainWindowService.OpenMainWindow();
+ if (_mainWindowService.HostScreen == null)
+ return;
+
+ // TODO: When proper routing has been implemented, use that here
+ // Create a settings VM to navigate to
+ SettingsViewModel settingsViewModel = _getSettingsViewModel(_mainWindowService.HostScreen);
+ // Get the release tab
+ ReleasesTabViewModel releaseTabViewModel = (ReleasesTabViewModel) settingsViewModel.SettingTabs.First(t => t is ReleasesTabViewModel);
+
+ // Navigate to the settings VM
+ _mainWindowService.HostScreen.Router.Navigate.Execute(settingsViewModel);
+ // Navigate to the release tab
+ releaseTabViewModel.PreselectId = releaseId;
+ settingsViewModel.SelectedTab = releaseTabViewModel;
+ });
+ }
+
+ private async Task InstallRelease(string releaseId, string releaseVersion)
+ {
+ ReleaseInstaller installer = _getReleaseInstaller(releaseId);
+ void InstallerOnPropertyChanged(object? sender, PropertyChangedEventArgs e) => UpdateInstallProgress(releaseId, installer);
+
+ GetBuilderForRelease(releaseId, releaseVersion)
+ .AddAudio(new ToastAudio {Silent = true})
+ .AddText("Installing Artemis update")
+ .AddVisualChild(new AdaptiveProgressBar()
+ {
+ Title = releaseVersion,
+ Value = new BindableProgressBarValue("progressValue"),
+ Status = new BindableString("progressStatus")
+ })
+ .AddButton(new ToastButton().SetContent("Cancel").AddArgument("action", "cancel"))
+ .Show(t =>
+ {
+ t.Tag = releaseId;
+ t.Data = GetDataForInstaller(installer);
+ });
+
+ // Wait for Windows animations to catch up to us, we fast!
+ await Task.Delay(2000);
+ _cancellationTokenSource = new CancellationTokenSource();
+ installer.PropertyChanged += InstallerOnPropertyChanged;
+ try
+ {
+ await installer.InstallAsync(_cancellationTokenSource.Token);
+ }
+ catch (Exception)
+ {
+ if (_cancellationTokenSource.IsCancellationRequested)
+ return;
+ throw;
+ }
+ finally
+ {
+ installer.PropertyChanged -= InstallerOnPropertyChanged;
+ }
+
+ // Queue an update in case the user interrupts the process after everything has been prepared
+ _updateService.QueueUpdate();
+
+ GetBuilderForRelease(releaseId, releaseVersion)
+ .AddAudio(new ToastAudio {Silent = true})
+ .AddText("Update ready")
+ .AddText($"Artemis version {releaseVersion} is ready to be applied")
+ .AddButton(new ToastButton().SetContent("Restart Artemis").AddArgument("action", "restart-for-update"))
+ .AddButton(new ToastButton().SetContent("Later").AddArgument("action", "postpone-update"))
+ .Show(t => t.Tag = releaseId);
+ }
+
+ private void UpdateInstallProgress(string releaseId, ReleaseInstaller installer)
+ {
+ ToastNotificationManagerCompat.CreateToastNotifier().Update(GetDataForInstaller(installer), releaseId);
+ }
+
+ private ToastContentBuilder GetBuilderForRelease(string releaseId, string releaseVersion)
+ {
+ return new ToastContentBuilder().AddArgument("releaseId", releaseId).AddArgument("releaseVersion", releaseVersion);
+ }
+
+ private NotificationData GetDataForInstaller(ReleaseInstaller installer)
+ {
+ NotificationData data = new()
+ {
+ Values =
+ {
+ ["progressValue"] = (installer.Progress / 100f).ToString(CultureInfo.InvariantCulture),
+ ["progressStatus"] = installer.Status
+ }
+ };
+
+ return data;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI.Windows/Scripts/update.ps1 b/src/Artemis.UI.Windows/Scripts/update.ps1
index 4247178dc..26fbd1165 100644
--- a/src/Artemis.UI.Windows/Scripts/update.ps1
+++ b/src/Artemis.UI.Windows/Scripts/update.ps1
@@ -4,6 +4,10 @@ param (
[Parameter(Mandatory=$false)][string]$artemisArgs
)
+Write-Host "Artemis update script v1"
+Write-Host "Please do not close this window, this should not take long"
+Write-Host ""
+
# Wait up to 10 seconds for the process to shut down
for ($i=1; $i -le 10; $i++) {
$process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue
@@ -26,12 +30,17 @@ if (!(Test-Path $destinationDirectory)) {
Write-Error "The destination directory does not exist"
}
-# If the destination directory exists, clear it
+# Clear the destination directory but don't remove it, leaving ACL entries in tact
+Write-Host "Cleaning up old version where needed"
Get-ChildItem $destinationDirectory | Remove-Item -Recurse -Force
# Move the contents of the source directory to the destination directory
+Write-Host "Installing new files"
Get-ChildItem $sourceDirectory | Move-Item -Destination $destinationDirectory
+# Remove the now empty source directory
+Remove-Item $sourceDirectory
+Write-Host "Finished! Restarting Artemis"
Start-Sleep -Seconds 1
# When finished, run the updated version
diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj
index 086380658..258b00486 100644
--- a/src/Artemis.UI/Artemis.UI.csproj
+++ b/src/Artemis.UI/Artemis.UI.csproj
@@ -34,8 +34,8 @@
-
-
+
+
@@ -43,4 +43,15 @@
+
+
+
+ UpdatingTabView.axaml
+ Code
+
+
+ UpdatingTabView.axaml
+ Code
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/DryIoc/ContainerExtensions.cs b/src/Artemis.UI/DryIoc/ContainerExtensions.cs
index 58970b2fd..cd7b167a5 100644
--- a/src/Artemis.UI/DryIoc/ContainerExtensions.cs
+++ b/src/Artemis.UI/DryIoc/ContainerExtensions.cs
@@ -17,7 +17,7 @@ namespace Artemis.UI.DryIoc;
///
/// Provides an extension method to register services onto a DryIoc .
///
-public static class UIContainerExtensions
+public static class ContainerExtensions
{
///
/// Registers UI services into the container.
@@ -25,20 +25,19 @@ public static class UIContainerExtensions
/// The builder building the current container
public static void RegisterUI(this IContainer container)
{
- Assembly[] thisAssembly = {typeof(UIContainerExtensions).Assembly};
+ Assembly[] thisAssembly = {typeof(ContainerExtensions).Assembly};
container.RegisterInstance(new AssetLoader(), IfAlreadyRegistered.Throw);
container.Register(Reuse.Singleton);
container.RegisterMany(thisAssembly, type => type.IsAssignableTo());
- container.RegisterMany(thisAssembly, type => type.IsAssignableTo(), ifAlreadyRegistered: IfAlreadyRegistered.Replace);
container.RegisterMany(thisAssembly, type => type.IsAssignableTo() && type.IsInterface);
container.RegisterMany(thisAssembly, type => type.IsAssignableTo() && type != typeof(PropertyVmFactory));
container.Register(Reuse.Singleton);
container.Register(Reuse.Singleton);
- container.Register();
-
+ container.Register();
+
container.RegisterMany(thisAssembly, type => type.IsAssignableTo(), Reuse.Singleton);
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs b/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs
index ed71b2c6e..8b3c5fa42 100644
--- a/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs
+++ b/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs
@@ -1,4 +1,5 @@
-using System.Collections.ObjectModel;
+using System;
+using System.Collections.ObjectModel;
using System.Reactive;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
@@ -17,6 +18,7 @@ using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers;
using Artemis.UI.Screens.Scripting;
using Artemis.UI.Screens.Settings;
+using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.VisualScripting;
@@ -474,4 +476,23 @@ public class ScriptVmFactory : IScriptVmFactory
{
return _container.Resolve(new object[] { profile, scriptConfiguration });
}
+}
+
+public interface IReleaseVmFactory : IVmFactory
+{
+ ReleaseViewModel ReleaseListViewModel(string releaseId, string version, DateTimeOffset createdAt);
+}
+public class ReleaseVmFactory : IReleaseVmFactory
+{
+ private readonly IContainer _container;
+
+ public ReleaseVmFactory(IContainer container)
+ {
+ _container = container;
+ }
+
+ public ReleaseViewModel ReleaseListViewModel(string releaseId, string version, DateTimeOffset createdAt)
+ {
+ return _container.Resolve(new object[] { releaseId, version, createdAt });
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs b/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs
index 20bb00ff2..9922665fc 100644
--- a/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs
+++ b/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs
@@ -14,7 +14,7 @@ namespace Artemis.UI.Screens.Debugger.Logs;
public class LogsDebugView : ReactiveUserControl
{
private int _lineCount;
- private TextEditor _textEditor;
+ private TextEditor? _textEditor;
public LogsDebugView()
{
@@ -31,7 +31,7 @@ public class LogsDebugView : ReactiveUserControl
protected override void OnInitialized()
{
base.OnInitialized();
- Dispatcher.UIThread.Post(() => _textEditor.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
+ Dispatcher.UIThread.Post(() => _textEditor?.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
}
private void OnTextChanged(object? sender, EventArgs e)
@@ -49,7 +49,7 @@ public class LogsDebugView : ReactiveUserControl
//we need this help distance because of rounding.
//if we scroll slightly above the end, we still want it
//to scroll down to the new lines.
- const double graceDistance = 1d;
+ const double GRACE_DISTANCE = 1d;
//if we were at the bottom of the log and
//if the last log event was 5 lines long
@@ -59,7 +59,7 @@ public class LogsDebugView : ReactiveUserControl
//if we are more than that out of sync,
//the user scrolled up and we should not
//mess with anything.
- if (_lineCount == 0 || linesAdded + graceDistance > outOfScreenLines)
+ if (_lineCount == 0 || linesAdded + GRACE_DISTANCE > outOfScreenLines)
{
Dispatcher.UIThread.Post(() => _textEditor.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
_lineCount = _textEditor.LineCount;
diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs
index fbd81e943..2037d0b1a 100644
--- a/src/Artemis.UI/Screens/Root/RootViewModel.cs
+++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs
@@ -58,7 +58,8 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
_lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!;
mainWindowService.ConfigureMainWindowProvider(this);
-
+ mainWindowService.HostScreen = this;
+
DisplayAccordingToSettings();
Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel);
Task.Run(() =>
@@ -230,11 +231,6 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
}
#endregion
-
- public void SaveWindowBounds(int x, int y, int width, int height)
- {
- throw new NotImplementedException();
- }
}
internal class EmptyViewModel : MainScreenViewModel
diff --git a/src/Artemis.UI/Screens/Settings/SettingsView.axaml b/src/Artemis.UI/Screens/Settings/SettingsView.axaml
index 96433edb6..78ac70be4 100644
--- a/src/Artemis.UI/Screens/Settings/SettingsView.axaml
+++ b/src/Artemis.UI/Screens/Settings/SettingsView.axaml
@@ -2,10 +2,12 @@
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:settings="clr-namespace:Artemis.UI.Screens.Settings"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
- x:Class="Artemis.UI.Screens.Settings.SettingsView">
+ x:Class="Artemis.UI.Screens.Settings.SettingsView"
+ x:DataType="settings:SettingsViewModel">
-
+
diff --git a/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs
index 0a372c29b..745ed277a 100644
--- a/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs
+++ b/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs
@@ -6,10 +6,13 @@ namespace Artemis.UI.Screens.Settings;
public class SettingsViewModel : MainScreenViewModel
{
+ private ActivatableViewModelBase _selectedTab;
+
public SettingsViewModel(IScreen hostScreen,
GeneralTabViewModel generalTabViewModel,
PluginsTabViewModel pluginsTabViewModel,
DevicesTabViewModel devicesTabViewModel,
+ ReleasesTabViewModel releasesTabViewModel,
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
{
SettingTabs = new ObservableCollection
@@ -17,9 +20,17 @@ public class SettingsViewModel : MainScreenViewModel
generalTabViewModel,
pluginsTabViewModel,
devicesTabViewModel,
+ releasesTabViewModel,
aboutTabViewModel
};
+ _selectedTab = generalTabViewModel;
}
public ObservableCollection SettingTabs { get; }
+
+ public ActivatableViewModelBase SelectedTab
+ {
+ get => _selectedTab;
+ set => RaiseAndSetIfChanged(ref _selectedTab, value);
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml
new file mode 100644
index 000000000..6528d8b04
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml.cs b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml.cs
new file mode 100644
index 000000000..3421db5a7
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml.cs
@@ -0,0 +1,17 @@
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Settings;
+
+public class ReleasesTabView : ReactiveUserControl
+{
+ public ReleasesTabView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs
new file mode 100644
index 000000000..d7e3a6517
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive.Linq;
+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.Services;
+using Artemis.UI.Shared.Services.Builders;
+using Artemis.WebClient.Updating;
+using Avalonia.Threading;
+using DynamicData;
+using DynamicData.Binding;
+using ReactiveUI;
+using Serilog;
+using StrawberryShake;
+
+namespace Artemis.UI.Screens.Settings;
+
+public class ReleasesTabViewModel : ActivatableViewModelBase
+{
+ private readonly ILogger _logger;
+ private readonly IUpdatingClient _updatingClient;
+ private readonly INotificationService _notificationService;
+ private readonly SourceList _releases;
+ private IGetReleases_PublishedReleases_PageInfo? _lastPageInfo;
+ private bool _loading;
+ private ReleaseViewModel? _selectedReleaseViewModel;
+
+ public ReleasesTabViewModel(ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, IReleaseVmFactory releaseVmFactory, INotificationService notificationService)
+ {
+ _logger = logger;
+ _updatingClient = updatingClient;
+ _notificationService = notificationService;
+
+ _releases = new SourceList();
+ _releases.Connect()
+ .Sort(SortExpressionComparer.Descending(p => p.CreatedAt))
+ .Transform(r => releaseVmFactory.ReleaseListViewModel(r.Id, r.Version, r.CreatedAt))
+ .ObserveOn(AvaloniaScheduler.Instance)
+ .Bind(out ReadOnlyObservableCollection releaseViewModels)
+ .Subscribe();
+
+ DisplayName = "Releases";
+ ReleaseViewModels = releaseViewModels;
+ this.WhenActivated(async d =>
+ {
+ await updateService.CacheLatestRelease();
+ await GetMoreReleases(d.AsCancellationToken());
+ SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(r => r.ReleaseId == PreselectId) ?? ReleaseViewModels.FirstOrDefault();
+ });
+ }
+
+ public ReadOnlyObservableCollection ReleaseViewModels { get; }
+ public string? PreselectId { get; set; }
+
+ public ReleaseViewModel? SelectedReleaseViewModel
+ {
+ get => _selectedReleaseViewModel;
+ set => RaiseAndSetIfChanged(ref _selectedReleaseViewModel, value);
+ }
+
+ public bool Loading
+ {
+ get => _loading;
+ private set => RaiseAndSetIfChanged(ref _loading, value);
+ }
+
+ public async Task GetMoreReleases(CancellationToken cancellationToken)
+ {
+ if (_lastPageInfo != null && !_lastPageInfo.HasNextPage)
+ return;
+
+ try
+ {
+ Loading = true;
+
+ IOperationResult result = await _updatingClient.GetReleases.ExecuteAsync("feature/gh-actions", Platform.Windows, 20, _lastPageInfo?.EndCursor, cancellationToken);
+ if (result.Data?.PublishedReleases?.Nodes == null)
+ return;
+
+ _lastPageInfo = result.Data.PublishedReleases.PageInfo;
+ _releases.AddRange(result.Data.PublishedReleases.Nodes);
+ }
+ catch (TaskCanceledException)
+ {
+ // ignored
+ }
+ catch (Exception e)
+ {
+ _logger.Warning(e, "Failed to retrieve releases");
+ _notificationService.CreateNotification()
+ .WithTitle("Failed to retrieve releases")
+ .WithMessage(e.Message)
+ .WithSeverity(NotificationSeverity.Warning)
+ .Show();
+ }
+ finally
+ {
+ Loading = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs
deleted file mode 100644
index ed0373da4..000000000
--- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Artemis.UI.Shared;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Avalonia.Markup.Xaml;
-using Avalonia.ReactiveUI;
-
-namespace Artemis.UI.Screens.Settings.Updating;
-
-public partial class ReleaseAvailableView : ReactiveCoreWindow
-{
- public ReleaseAvailableView()
- {
- InitializeComponent();
-#if DEBUG
- this.AttachDevTools();
-#endif
- }
-
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
- }
-
- private void Button_OnClick(object? sender, RoutedEventArgs e)
- {
- Close();
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs
deleted file mode 100644
index 118177c7f..000000000
--- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using System.Reactive;
-using System.Reactive.Linq;
-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.WebClient.Updating;
-using ReactiveUI;
-using Serilog;
-using StrawberryShake;
-
-namespace Artemis.UI.Screens.Settings.Updating;
-
-public class ReleaseAvailableViewModel : ActivatableViewModelBase
-{
- private readonly string _nextReleaseId;
- private readonly ILogger _logger;
- private readonly IUpdateService _updateService;
- private readonly IUpdatingClient _updatingClient;
- private readonly INotificationService _notificationService;
- private IGetReleaseById_Release? _release;
-
- public ReleaseAvailableViewModel(string nextReleaseId, ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, INotificationService notificationService)
- {
- _nextReleaseId = nextReleaseId;
- _logger = logger;
- _updateService = updateService;
- _updatingClient = updatingClient;
- _notificationService = notificationService;
-
- CurrentVersion = _updateService.CurrentVersion ?? "Development build";
- Install = ReactiveCommand.Create(ExecuteInstall, this.WhenAnyValue(vm => vm.Release).Select(r => r != null));
-
- this.WhenActivated(async d => await RetrieveRelease(d.AsCancellationToken()));
- }
-
- private void ExecuteInstall()
- {
- _updateService.InstallRelease(_nextReleaseId);
- }
-
- private async Task RetrieveRelease(CancellationToken cancellationToken)
- {
- IOperationResult result = await _updatingClient.GetReleaseById.ExecuteAsync(_nextReleaseId, cancellationToken);
- // Borrow GraphQLClientException for messaging, how lazy of me..
- if (result.Errors.Count > 0)
- {
- GraphQLClientException exception = new(result.Errors);
- _logger.Error(exception, "Failed to retrieve release details");
- _notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage(exception.Message).Show();
- return;
- }
-
- if (result.Data?.Release == null)
- {
- _notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage("Release not found").Show();
- return;
- }
-
- Release = result.Data.Release;
- }
-
- public string CurrentVersion { get; }
-
- public IGetReleaseById_Release? Release
- {
- get => _release;
- set => RaiseAndSetIfChanged(ref _release, value);
- }
-
- public ReactiveCommand Install { get; }
- public ReactiveCommand AskLater { get; }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml
deleted file mode 100644
index 46699f5ae..000000000
--- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
- Downloading & installing update...
-
-
-
- This should not take long, when finished Artemis must restart.
-
-
-
-
-
-
-
- Done, click restart to apply the update 🫡
-
-
- Restart when finished
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs
deleted file mode 100644
index ec2a689e6..000000000
--- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using Artemis.UI.Shared;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Avalonia.Markup.Xaml;
-
-namespace Artemis.UI.Screens.Settings.Updating;
-
-public partial class ReleaseInstallerView : ReactiveCoreWindow
-{
- public ReleaseInstallerView()
- {
- InitializeComponent();
-#if DEBUG
- this.AttachDevTools();
-#endif
- }
-
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
- }
-
- private void Cancel_OnClick(object? sender, RoutedEventArgs e)
- {
- Close();
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs
deleted file mode 100644
index 79a80aa06..000000000
--- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using System;
-using System.Reactive;
-using System.Reactive.Disposables;
-using System.Reactive.Linq;
-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 ReactiveUI;
-
-namespace Artemis.UI.Screens.Settings.Updating;
-
-public class ReleaseInstallerViewModel : ActivatableViewModelBase
-{
- private readonly ReleaseInstaller _releaseInstaller;
- private readonly IWindowService _windowService;
- private ObservableAsPropertyHelper? _overallProgress;
- private ObservableAsPropertyHelper? _stepProgress;
- private bool _ready;
- private bool _restartWhenFinished;
-
- public ReleaseInstallerViewModel(ReleaseInstaller releaseInstaller, IWindowService windowService)
- {
- _releaseInstaller = releaseInstaller;
- _windowService = windowService;
-
- Restart = ReactiveCommand.Create(() => Utilities.ApplyUpdate(false));
- this.WhenActivated(d =>
- {
- _overallProgress = Observable.FromEventPattern(x => _releaseInstaller.OverallProgress.ProgressChanged += x, x => _releaseInstaller.OverallProgress.ProgressChanged -= x)
- .Select(e => e.EventArgs)
- .ToProperty(this, vm => vm.OverallProgress)
- .DisposeWith(d);
- _stepProgress = Observable.FromEventPattern(x => _releaseInstaller.StepProgress.ProgressChanged += x, x => _releaseInstaller.StepProgress.ProgressChanged -= x)
- .Select(e => e.EventArgs)
- .ToProperty(this, vm => vm.StepProgress)
- .DisposeWith(d);
-
- Task.Run(() => InstallUpdate(d.AsCancellationToken()));
- });
- }
-
- public ReactiveCommand Restart { get; }
-
- public float OverallProgress => _overallProgress?.Value ?? 0;
- public float StepProgress => _stepProgress?.Value ?? 0;
-
- public bool Ready
- {
- get => _ready;
- set => RaiseAndSetIfChanged(ref _ready, value);
- }
-
- public bool RestartWhenFinished
- {
- get => _restartWhenFinished;
- set => RaiseAndSetIfChanged(ref _restartWhenFinished, value);
- }
-
- private async Task InstallUpdate(CancellationToken cancellationToken)
- {
- try
- {
- await _releaseInstaller.InstallAsync(cancellationToken);
- Ready = true;
- if (RestartWhenFinished)
- Utilities.ApplyUpdate(false);
- }
- catch (TaskCanceledException)
- {
- // ignored
- }
- catch (Exception e)
- {
- _windowService.ShowExceptionDialog("Something went wrong while installing the update", e);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml
similarity index 50%
rename from src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml
rename to src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml
index acf70d5b4..91e1a0a4c 100644
--- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml
+++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml
@@ -1,49 +1,163 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- A new Artemis update is available! 🥳
-
+
+
+
+
+ Release info
-
- Retrieving release...
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
+
+
+
+ Ready, restart to install
+
+
+
+
+
-
+
+
+
+
+
+ Release date
+
+
+
+
+
+ Source
+
+
+
+
+
+ File size
+
+
+
+
+
+
+
+
+
+ Release notes
+
+
+