diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml index 569228685..96cbdbc6e 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml @@ -455,17 +455,17 @@ - Represents the base class for Artemis view models + Represents the base class for Artemis view models - Gets or sets the display name of the view model + Gets or sets the display name of the view model - Represents the base class for Artemis view models that are interested in the activated event + Represents the base class for Artemis view models that are interested in the activated event @@ -473,11 +473,11 @@ - Releases the unmanaged resources used by the object and optionally releases the managed resources. + Releases the unmanaged resources used by the object and optionally releases the managed resources. - to release both managed and unmanaged resources; - to release only unmanaged resources. + to release both managed and unmanaged resources; + to release only unmanaged resources. @@ -488,20 +488,18 @@ - Represents the base class for Artemis view models used to drive dialogs + Represents the base class for Artemis view models used to drive dialogs - - - - + - Closes the dialog with a given result + Closes the dialog with the given + The result of the dialog - + - Closes the dialog without a result + Closes the dialog without a result diff --git a/src/Artemis.UI.Avalonia.Shared/Events/DialogClosedEventArgs.cs b/src/Artemis.UI.Avalonia.Shared/Events/DialogClosedEventArgs.cs new file mode 100644 index 000000000..0a5aa0755 --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Events/DialogClosedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Artemis.UI.Avalonia.Shared.Events +{ + internal class DialogClosedEventArgs : EventArgs + { + public TResult Result { get; } + + public DialogClosedEventArgs(TResult result) + { + Result = result; + } + } +} diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs b/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs index 7cd562f7c..ebfbddbff 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Layout; using Avalonia.Threading; using FluentAvalonia.UI.Controls; using ReactiveUI; @@ -17,7 +18,12 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders public NotificationBuilder(Window parent) { _parent = parent; - _infoBar = new InfoBar {Classes = Classes.Parse("notification-info-bar")}; + _infoBar = new InfoBar + { + Classes = Classes.Parse("notification-info-bar"), + VerticalAlignment = VerticalAlignment.Bottom, + HorizontalAlignment = HorizontalAlignment.Right + }; } public NotificationBuilder WithTitle(string? title) @@ -38,6 +44,17 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders return this; } + public NotificationBuilder WithVerticalPosition(VerticalAlignment position) + { + _infoBar.VerticalAlignment = position; + return this; + } + + public NotificationBuilder WithHorizontalPosition(HorizontalAlignment position) + { + _infoBar.HorizontalAlignment = position; + return this; + } /// /// Add a filter to the dialog @@ -105,8 +122,8 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders public IControl Build() { - return _action != null - ? new Button {Content = _text, Command = ReactiveCommand.Create(() => _action)} + return _action != null + ? new Button {Content = _text, Command = ReactiveCommand.Create(() => _action)} : new Button {Content = _text}; } } diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml index 0fc655a38..8211265bb 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml +++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml @@ -2,8 +2,47 @@ 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" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" x:Class="Artemis.UI.Avalonia.Shared.Services.ExceptionDialogView" - Title="ExceptionDialogView"> - Eh you got an exception but I didn't write the viewer yet :( - + Title="{Binding Title}" + ExtendClientAreaToDecorationsHint="True" + Width="800" + Height="800" + WindowStartupLocation="CenterOwner"> + + + + + + + + Awww :( + + It looks like Artemis ran into an unhandled exception. If this keeps happening feel free to hit us up on Discord. + + + + + + + + + When reporting errors please don't take a screenshot of the error, instead copy the text, thanks! + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs index 773c3f6d7..9af31290c 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs @@ -4,7 +4,7 @@ using Avalonia.ReactiveUI; namespace Artemis.UI.Avalonia.Shared.Services { - public partial class ExceptionDialogView : ReactiveWindow + internal class ExceptionDialogView : ReactiveWindow { public ExceptionDialogView() { @@ -19,4 +19,4 @@ namespace Artemis.UI.Avalonia.Shared.Services AvaloniaXamlLoader.Load(this); } } -} +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs index a43a2c1f4..66fb9bdeb 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs @@ -1,12 +1,35 @@ using System; +using System.Threading.Tasks; +using Artemis.UI.Avalonia.Shared.Services.Builders; +using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Avalonia; +using Avalonia.Layout; namespace Artemis.UI.Avalonia.Shared.Services { - public class ExceptionDialogViewModel : DialogViewModelBase + internal class ExceptionDialogViewModel : DialogViewModelBase { - public ExceptionDialogViewModel(string title, Exception exception) + private readonly INotificationService _notificationService; + + public ExceptionDialogViewModel(string title, Exception exception, INotificationService notificationService) { - + _notificationService = notificationService; + + Title = $"Artemis | {title}"; + Exception = exception; + } + + public string Title { get; } + public Exception Exception { get; } + + public async Task CopyException() + { + await Application.Current.Clipboard.SetTextAsync(Exception.ToString()); + _notificationService.CreateNotification() + .WithMessage("Copied stack trace to clipboard.") + .WithSeverity(NotificationSeverity.Success) + .WithHorizontalPosition(HorizontalAlignment.Center) + .Show(); } } -} +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs index e11416551..9cb66b8a7 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.UI.Avalonia.Shared.Exceptions; using Artemis.UI.Avalonia.Shared.Services.Builders; @@ -7,9 +8,11 @@ using Artemis.UI.Avalonia.Shared.Services.Interfaces; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; using FluentAvalonia.UI.Controls; using Ninject; using Ninject.Parameters; +using ReactiveUI; namespace Artemis.UI.Avalonia.Shared.Services { @@ -91,25 +94,30 @@ namespace Artemis.UI.Avalonia.Shared.Services Window window = (Window) Activator.CreateInstance(type)!; window.DataContext = viewModel; + viewModel.CloseRequested += (_, args) => window.Close(args.Result); + viewModel.CancelRequested += (_, _) => window.Close(); + return await window.ShowDialog(parent); } public void ShowExceptionDialog(string title, Exception exception) { if (_exceptionDialogOpen) - { return; - } - try + _exceptionDialogOpen = true; + // Fire and forget the dialog + Dispatcher.UIThread.InvokeAsync(async () => { - _exceptionDialogOpen = true; - ShowDialogAsync(new ExceptionDialogViewModel(title, exception)).GetAwaiter().GetResult(); - } - finally - { - _exceptionDialogOpen = false; - } + try + { + await ShowDialogAsync(new ExceptionDialogViewModel(title, exception, _kernel.Get())); + } + finally + { + _exceptionDialogOpen = false; + } + }); } public ContentDialogBuilder CreateContentDialog() diff --git a/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml b/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml index e58b3b634..c3be2accd 100644 --- a/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml +++ b/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml @@ -13,9 +13,6 @@ - - - @@ -25,4 +22,7 @@ + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs b/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs index c0a04abda..1bb568963 100644 --- a/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs +++ b/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs @@ -1,19 +1,19 @@ using System; -using System.Reactive; using System.Reactive.Disposables; +using Artemis.UI.Avalonia.Shared.Events; using ReactiveUI; namespace Artemis.UI.Avalonia.Shared { /// - /// Represents the base class for Artemis view models + /// Represents the base class for Artemis view models /// public abstract class ViewModelBase : ReactiveObject { private string? _displayName; /// - /// Gets or sets the display name of the view model + /// Gets or sets the display name of the view model /// public string? DisplayName { @@ -23,7 +23,7 @@ namespace Artemis.UI.Avalonia.Shared } /// - /// Represents the base class for Artemis view models that are interested in the activated event + /// Represents the base class for Artemis view models that are interested in the activated event /// public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel, IDisposable { @@ -34,11 +34,11 @@ namespace Artemis.UI.Avalonia.Shared } /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. /// protected virtual void Dispose(bool disposing) { @@ -56,25 +56,28 @@ namespace Artemis.UI.Avalonia.Shared } /// - /// Represents the base class for Artemis view models used to drive dialogs + /// Represents the base class for Artemis view models used to drive dialogs /// public abstract class DialogViewModelBase : ActivatableViewModelBase { - /// - protected DialogViewModelBase() + /// + /// Closes the dialog with the given + /// + /// The result of the dialog + public void Close(TResult result) { - Close = ReactiveCommand.Create(t => t); - Cancel = ReactiveCommand.Create(() => { }); + CloseRequested?.Invoke(this, new DialogClosedEventArgs(result)); } /// - /// Closes the dialog with a given result + /// Closes the dialog without a result /// - public ReactiveCommand Close { get; } + public void Cancel() + { + CancelRequested?.Invoke(this, EventArgs.Empty); + } - /// - /// Closes the dialog without a result - /// - public ReactiveCommand Cancel { get; } + internal event EventHandler>? CloseRequested; + internal event EventHandler? CancelRequested; } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj.DotSettings b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj.DotSettings new file mode 100644 index 000000000..452c5acb6 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs index 2e47658c4..af0818afd 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs @@ -118,7 +118,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels public void Accept() { - Close.Execute(true); + Close(true); } public static async Task Show(IWindowService windowService, List subjects) diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs index a14426227..238cd309e 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs @@ -105,7 +105,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels // This shouldn't be happening and the experience isn't very nice for the user (too lazy to make a nice UI for such an edge case) // but at least give some feedback - Close.Execute(false); + Close(false); await _windowService.CreateContentDialog() .WithTitle("Plugin prerequisites") .WithContent("The plugin was not able to fully remove all prerequisites. \r\nPlease try again or contact the plugin creator.") @@ -126,7 +126,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels public void Accept() { - Close.Execute(true); + Close(true); } public static async Task Show(IWindowService windowService, List subjects, string cancelLabel = "Cancel") diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs index 3dbad61ee..9ae740a19 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs @@ -12,6 +12,7 @@ using Artemis.UI.Avalonia.Exceptions; using Artemis.UI.Avalonia.Ninject.Factories; using Artemis.UI.Avalonia.Shared; using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Avalonia.Threading; using Ninject; using ReactiveUI; @@ -53,9 +54,13 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels _pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginToggled; OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings, this.WhenAnyValue(x => x.IsEnabled).Select(isEnabled => isEnabled && Plugin.ConfigurationDialog != null)); + InstallPrerequisites = ReactiveCommand.CreateFromTask(ExecuteInstallPrerequisites, this.WhenAnyValue(x => x.CanInstallPrerequisites)); + RemovePrerequisites = ReactiveCommand.CreateFromTask(ExecuteRemovePrerequisites, this.WhenAnyValue(x => x.CanRemovePrerequisites)); } public ReactiveCommand OpenSettings { get; } + public ReactiveCommand InstallPrerequisites { get; } + public ReactiveCommand RemovePrerequisites { get; } public ObservableCollection PluginFeatures { get; } @@ -72,7 +77,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels } public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name; - + public bool IsEnabled { get => Plugin.IsEnabled; @@ -137,7 +142,8 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels { bool wasEnabled = IsEnabled; - _pluginManagementService.UnloadPlugin(Plugin); + await Task.Run(() => _pluginManagementService.UnloadPlugin(Plugin)); + PluginFeatures.Clear(); Plugin = _pluginManagementService.LoadPlugin(Plugin.Directory); @@ -150,7 +156,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels _notificationService.CreateNotification().WithTitle("Reloaded plugin.").Show(); } - public async Task InstallPrerequisites() + public async Task ExecuteInstallPrerequisites() { List subjects = new() {Plugin.Info}; subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled)); @@ -159,7 +165,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); } - public async Task RemovePrerequisites(bool forPluginRemoval = false) + public async Task ExecuteRemovePrerequisites(bool forPluginRemoval = false) { List subjects = new() {Plugin.Info}; subjects.AddRange(!forPluginRemoval ? Plugin.Features.Where(f => f.AlwaysEnabled) : Plugin.Features); @@ -200,7 +206,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels List subjects = new() {Plugin.Info}; subjects.AddRange(Plugin.Features); if (subjects.Any(s => s.Prerequisites.Any(p => p.UninstallActions.Any()))) - await RemovePrerequisites(true); + await ExecuteRemovePrerequisites(true); try { @@ -284,7 +290,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels } } - await Task.Run(() => + await Task.Run(async () => { try { @@ -292,10 +298,10 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels } catch (Exception e) { - _notificationService.CreateNotification() + await Dispatcher.UIThread.InvokeAsync(() => _notificationService.CreateNotification() .WithMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}") .HavingButton(b => b.WithText("View logs").WithAction(ShowLogsFolder)) - .Show(); + .Show()); } finally { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml index 9100a1af1..4f990e857 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml @@ -27,12 +27,10 @@ Icon="{Binding FeatureInfo.ResolvedIcon}" Width="20" Height="20" - IsVisible="{Binding LoadException, Converter={x:Static ObjectConverters.IsNull}}" />