1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

UI - Implemented exception dialog

UI - Fixed notification opacity
UI - Added position control to notifications
This commit is contained in:
Robert 2021-11-13 17:46:41 +01:00
parent b963aa0909
commit 63eb0ca9b3
15 changed files with 187 additions and 84 deletions

View File

@ -455,17 +455,17 @@
</member>
<member name="T:Artemis.UI.Avalonia.Shared.ViewModelBase">
<summary>
Represents the base class for Artemis view models
Represents the base class for Artemis view models
</summary>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.ViewModelBase.DisplayName">
<summary>
Gets or sets the display name of the view model
Gets or sets the display name of the view model
</summary>
</member>
<member name="T:Artemis.UI.Avalonia.Shared.ActivatableViewModelBase">
<summary>
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
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.ActivatableViewModelBase.#ctor">
@ -473,11 +473,11 @@
</member>
<member name="M:Artemis.UI.Avalonia.Shared.ActivatableViewModelBase.Dispose(System.Boolean)">
<summary>
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.
</summary>
<param name="disposing">
<see langword="true" /> to release both managed and unmanaged resources;
<see langword="false" /> to release only unmanaged resources.
<see langword="true" /> to release both managed and unmanaged resources;
<see langword="false" /> to release only unmanaged resources.
</param>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.ActivatableViewModelBase.Activator">
@ -488,20 +488,18 @@
</member>
<member name="T:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1">
<summary>
Represents the base class for Artemis view models used to drive dialogs
Represents the base class for Artemis view models used to drive dialogs
</summary>
</member>
<member name="M:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1.#ctor">
<inheritdoc />
</member>
<member name="P:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1.Close">
<member name="M:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1.Close(`0)">
<summary>
Closes the dialog with a given result
Closes the dialog with the given <paramref name="result" />
</summary>
<param name="result">The result of the dialog</param>
</member>
<member name="P:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1.Cancel">
<member name="M:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1.Cancel">
<summary>
Closes the dialog without a result
Closes the dialog without a result
</summary>
</member>
</members>

View File

@ -0,0 +1,14 @@
using System;
namespace Artemis.UI.Avalonia.Shared.Events
{
internal class DialogClosedEventArgs<TResult> : EventArgs
{
public TResult Result { get; }
public DialogClosedEventArgs(TResult result)
{
Result = result;
}
}
}

View File

@ -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;
}
/// <summary>
/// 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};
}
}

View File

@ -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 :(
</Window>
Title="{Binding Title}"
ExtendClientAreaToDecorationsHint="True"
Width="800"
Height="800"
WindowStartupLocation="CenterOwner">
<Grid>
<TextBlock Margin="10" IsHitTestVisible="False" Text="{Binding Title}" />
<Grid Margin="0 32 0 0" ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
<Border Classes="card" Grid.Row="0" Grid.ColumnSpan="2" Margin="10">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Margin="0 5">
<Grid RowDefinitions="Auto,Auto,*">
<TextBlock Grid.Row="0" Classes="h3">Awww :(</TextBlock>
<TextBlock Grid.Row="1">
It looks like Artemis ran into an unhandled exception. If this keeps happening feel free to hit us up on Discord.
</TextBlock>
<TextBox Grid.Row="2" Text="{Binding Exception, Mode=OneTime}"
Margin="0 10"
AcceptsReturn="True"
IsReadOnly="True"
FontFamily="Consolas"
FontSize="12"
BorderThickness="0" />
</Grid>
</ScrollViewer>
</Border>
<TextBlock Grid.Row="1" Grid.Column="0" TextWrapping="Wrap" Margin="15" VerticalAlignment="Center">
When reporting errors please don't take a screenshot of the error, instead copy the text, thanks!
</TextBlock>
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="15">
<Button Command="{Binding CopyException}" Classes="AppBarButton" Width="150" Margin="0 0 5 0">
Copy exception
</Button>
<Button Command="{Binding Close}" Width="150" Margin="5 0 0 0">
Close
</Button>
</StackPanel>
</Grid>
</Grid>
</Window>

View File

@ -4,7 +4,7 @@ using Avalonia.ReactiveUI;
namespace Artemis.UI.Avalonia.Shared.Services
{
public partial class ExceptionDialogView : ReactiveWindow<ExceptionDialogViewModel>
internal class ExceptionDialogView : ReactiveWindow<ExceptionDialogViewModel>
{
public ExceptionDialogView()
{
@ -19,4 +19,4 @@ namespace Artemis.UI.Avalonia.Shared.Services
AvaloniaXamlLoader.Load(this);
}
}
}
}

View File

@ -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<object>
internal class ExceptionDialogViewModel : DialogViewModelBase<object>
{
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();
}
}
}
}

View File

@ -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<TResult>(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<INotificationService>()));
}
finally
{
_exceptionDialogOpen = false;
}
});
}
public ContentDialogBuilder CreateContentDialog()

View File

@ -13,9 +13,6 @@
<Setter Property="Opacity" Value="0" />
<Setter Property="MaxWidth" Value="600" />
<Setter Property="Margin" Value="15"/>
<Setter Property="Background" Value="#25a3a3a3"/>
<Setter Property="VerticalAlignment" Value="Bottom" />
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.2"/>
@ -25,4 +22,7 @@
<Style Selector="controls|InfoBar.notification-info-bar[IsOpen=True]">
<Setter Property="Opacity" Value="1" />
</Style>
<Style Selector="controls|InfoBar.notification-info-bar:informational /template/ Border#ContentRoot">
<Setter Property="Background" Value="#ff3c3c3c" />
</Style>
</Styles>

View File

@ -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
{
/// <summary>
/// Represents the base class for Artemis view models
/// Represents the base class for Artemis view models
/// </summary>
public abstract class ViewModelBase : ReactiveObject
{
private string? _displayName;
/// <summary>
/// Gets or sets the display name of the view model
/// Gets or sets the display name of the view model
/// </summary>
public string? DisplayName
{
@ -23,7 +23,7 @@ namespace Artemis.UI.Avalonia.Shared
}
/// <summary>
/// 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
/// </summary>
public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel, IDisposable
{
@ -34,11 +34,11 @@ namespace Artemis.UI.Avalonia.Shared
}
/// <summary>
/// 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.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
@ -56,25 +56,28 @@ namespace Artemis.UI.Avalonia.Shared
}
/// <summary>
/// Represents the base class for Artemis view models used to drive dialogs
/// Represents the base class for Artemis view models used to drive dialogs
/// </summary>
public abstract class DialogViewModelBase<TResult> : ActivatableViewModelBase
{
/// <inheritdoc />
protected DialogViewModelBase()
/// <summary>
/// Closes the dialog with the given <paramref name="result" />
/// </summary>
/// <param name="result">The result of the dialog</param>
public void Close(TResult result)
{
Close = ReactiveCommand.Create<TResult, TResult>(t => t);
Cancel = ReactiveCommand.Create(() => { });
CloseRequested?.Invoke(this, new DialogClosedEventArgs<TResult>(result));
}
/// <summary>
/// Closes the dialog with a given result
/// Closes the dialog without a result
/// </summary>
public ReactiveCommand<TResult, TResult> Close { get; }
public void Cancel()
{
CancelRequested?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Closes the dialog without a result
/// </summary>
public ReactiveCommand<Unit, Unit> Cancel { get; }
internal event EventHandler<DialogClosedEventArgs<TResult>>? CloseRequested;
internal event EventHandler? CancelRequested;
}
}

View File

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -118,7 +118,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels
public void Accept()
{
Close.Execute(true);
Close(true);
}
public static async Task<bool> Show(IWindowService windowService, List<IPrerequisitesSubject> subjects)

View File

@ -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<object> Show(IWindowService windowService, List<IPrerequisitesSubject> subjects, string cancelLabel = "Cancel")

View File

@ -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<bool>(ExecuteRemovePrerequisites, this.WhenAnyValue(x => x.CanRemovePrerequisites));
}
public ReactiveCommand<Unit, Unit> OpenSettings { get; }
public ReactiveCommand<Unit, Unit> InstallPrerequisites { get; }
public ReactiveCommand<bool, Unit> RemovePrerequisites { get; }
public ObservableCollection<PluginFeatureViewModel> 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<IPrerequisitesSubject> 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<IPrerequisitesSubject> 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<IPrerequisitesSubject> 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
{

View File

@ -27,12 +27,10 @@
Icon="{Binding FeatureInfo.ResolvedIcon}"
Width="20"
Height="20"
IsVisible="{Binding LoadException, Converter={x:Static ObjectConverters.IsNull}}" />
<Button Grid.Column="0"
Margin="-8"
Classes="AppBarButton icon-button"
IsVisible="{Binding LoadException, Converter={x:Static ObjectConverters.IsNotNull}}"
Foreground="#E74C4C"
ToolTip.Tip="An exception occurred while enabling this feature, click to view"

View File

@ -1,5 +1,6 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.UI.Avalonia.Screens.Plugins.ViewModels;
using Avalonia;
using Avalonia.Markup.Xaml;
@ -19,22 +20,16 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.Views
this.WhenActivated(disposables =>
{
ViewModel!.ConfigurationViewModel.CloseRequested += ConfigurationViewModelOnCloseRequested;
Disposable.Create(HandleDeactivation).DisposeWith(disposables);
Observable.FromEventPattern(
x => ViewModel!.ConfigurationViewModel.CloseRequested += x,
x => ViewModel!.ConfigurationViewModel.CloseRequested -= x
)
.Subscribe(_ => Close())
.DisposeWith(disposables);
}
);
}
private void HandleDeactivation()
{
ViewModel!.ConfigurationViewModel.CloseRequested -= ConfigurationViewModelOnCloseRequested;
}
private void ConfigurationViewModelOnCloseRequested(object? sender, EventArgs e)
{
Close();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);