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 + + +