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

@ -491,15 +491,13 @@
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> </summary>
</member> </member>
<member name="M:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1.#ctor"> <member name="M:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1.Close(`0)">
<inheritdoc />
</member>
<member name="P:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1.Close">
<summary> <summary>
Closes the dialog with a given result Closes the dialog with the given <paramref name="result" />
</summary> </summary>
<param name="result">The result of the dialog</param>
</member> </member>
<member name="P:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1.Cancel"> <member name="M:Artemis.UI.Avalonia.Shared.DialogViewModelBase`1.Cancel">
<summary> <summary>
Closes the dialog without a result Closes the dialog without a result
</summary> </summary>

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;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Threading; using Avalonia.Threading;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using ReactiveUI; using ReactiveUI;
@ -17,7 +18,12 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders
public NotificationBuilder(Window parent) public NotificationBuilder(Window parent)
{ {
_parent = 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) public NotificationBuilder WithTitle(string? title)
@ -38,6 +44,17 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders
return this; return this;
} }
public NotificationBuilder WithVerticalPosition(VerticalAlignment position)
{
_infoBar.VerticalAlignment = position;
return this;
}
public NotificationBuilder WithHorizontalPosition(HorizontalAlignment position)
{
_infoBar.HorizontalAlignment = position;
return this;
}
/// <summary> /// <summary>
/// Add a filter to the dialog /// Add a filter to the dialog

View File

@ -2,8 +2,47 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 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" x:Class="Artemis.UI.Avalonia.Shared.Services.ExceptionDialogView"
Title="ExceptionDialogView"> Title="{Binding Title}"
Eh you got an exception but I didn't write the viewer yet :( 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> </Window>

View File

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

View File

@ -1,12 +1,35 @@
using System; 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 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;
using System.Linq; using System.Linq;
using System.Reactive.Disposables;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.UI.Avalonia.Shared.Exceptions; using Artemis.UI.Avalonia.Shared.Exceptions;
using Artemis.UI.Avalonia.Shared.Services.Builders; using Artemis.UI.Avalonia.Shared.Services.Builders;
@ -7,9 +8,11 @@ using Artemis.UI.Avalonia.Shared.Services.Interfaces;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Ninject; using Ninject;
using Ninject.Parameters; using Ninject.Parameters;
using ReactiveUI;
namespace Artemis.UI.Avalonia.Shared.Services namespace Artemis.UI.Avalonia.Shared.Services
{ {
@ -91,25 +94,30 @@ namespace Artemis.UI.Avalonia.Shared.Services
Window window = (Window) Activator.CreateInstance(type)!; Window window = (Window) Activator.CreateInstance(type)!;
window.DataContext = viewModel; window.DataContext = viewModel;
viewModel.CloseRequested += (_, args) => window.Close(args.Result);
viewModel.CancelRequested += (_, _) => window.Close();
return await window.ShowDialog<TResult>(parent); return await window.ShowDialog<TResult>(parent);
} }
public void ShowExceptionDialog(string title, Exception exception) public void ShowExceptionDialog(string title, Exception exception)
{ {
if (_exceptionDialogOpen) if (_exceptionDialogOpen)
{
return; return;
}
_exceptionDialogOpen = true;
// Fire and forget the dialog
Dispatcher.UIThread.InvokeAsync(async () =>
{
try try
{ {
_exceptionDialogOpen = true; await ShowDialogAsync(new ExceptionDialogViewModel(title, exception, _kernel.Get<INotificationService>()));
ShowDialogAsync(new ExceptionDialogViewModel(title, exception)).GetAwaiter().GetResult();
} }
finally finally
{ {
_exceptionDialogOpen = false; _exceptionDialogOpen = false;
} }
});
} }
public ContentDialogBuilder CreateContentDialog() public ContentDialogBuilder CreateContentDialog()

View File

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

View File

@ -1,6 +1,6 @@
using System; using System;
using System.Reactive;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Artemis.UI.Avalonia.Shared.Events;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Avalonia.Shared namespace Artemis.UI.Avalonia.Shared
@ -60,21 +60,24 @@ namespace Artemis.UI.Avalonia.Shared
/// </summary> /// </summary>
public abstract class DialogViewModelBase<TResult> : ActivatableViewModelBase public abstract class DialogViewModelBase<TResult> : ActivatableViewModelBase
{ {
/// <inheritdoc />
protected DialogViewModelBase()
{
Close = ReactiveCommand.Create<TResult, TResult>(t => t);
Cancel = ReactiveCommand.Create(() => { });
}
/// <summary> /// <summary>
/// Closes the dialog with a given result /// Closes the dialog with the given <paramref name="result" />
/// </summary> /// </summary>
public ReactiveCommand<TResult, TResult> Close { get; } /// <param name="result">The result of the dialog</param>
public void Close(TResult result)
{
CloseRequested?.Invoke(this, new DialogClosedEventArgs<TResult>(result));
}
/// <summary> /// <summary>
/// Closes the dialog without a result /// Closes the dialog without a result
/// </summary> /// </summary>
public ReactiveCommand<Unit, Unit> Cancel { get; } public void Cancel()
{
CancelRequested?.Invoke(this, EventArgs.Empty);
}
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() public void Accept()
{ {
Close.Execute(true); Close(true);
} }
public static async Task<bool> Show(IWindowService windowService, List<IPrerequisitesSubject> subjects) 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) // 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 // but at least give some feedback
Close.Execute(false); Close(false);
await _windowService.CreateContentDialog() await _windowService.CreateContentDialog()
.WithTitle("Plugin prerequisites") .WithTitle("Plugin prerequisites")
.WithContent("The plugin was not able to fully remove all prerequisites. \r\nPlease try again or contact the plugin creator.") .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() public void Accept()
{ {
Close.Execute(true); Close(true);
} }
public static async Task<object> Show(IWindowService windowService, List<IPrerequisitesSubject> subjects, string cancelLabel = "Cancel") 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.Ninject.Factories;
using Artemis.UI.Avalonia.Shared; using Artemis.UI.Avalonia.Shared;
using Artemis.UI.Avalonia.Shared.Services.Interfaces; using Artemis.UI.Avalonia.Shared.Services.Interfaces;
using Avalonia.Threading;
using Ninject; using Ninject;
using ReactiveUI; using ReactiveUI;
@ -53,9 +54,13 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels
_pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginToggled; _pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginToggled;
OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings, this.WhenAnyValue(x => x.IsEnabled).Select(isEnabled => isEnabled && Plugin.ConfigurationDialog != null)); 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> OpenSettings { get; }
public ReactiveCommand<Unit, Unit> InstallPrerequisites { get; }
public ReactiveCommand<bool, Unit> RemovePrerequisites { get; }
public ObservableCollection<PluginFeatureViewModel> PluginFeatures { get; } public ObservableCollection<PluginFeatureViewModel> PluginFeatures { get; }
@ -137,7 +142,8 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels
{ {
bool wasEnabled = IsEnabled; bool wasEnabled = IsEnabled;
_pluginManagementService.UnloadPlugin(Plugin); await Task.Run(() => _pluginManagementService.UnloadPlugin(Plugin));
PluginFeatures.Clear(); PluginFeatures.Clear();
Plugin = _pluginManagementService.LoadPlugin(Plugin.Directory); Plugin = _pluginManagementService.LoadPlugin(Plugin.Directory);
@ -150,7 +156,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels
_notificationService.CreateNotification().WithTitle("Reloaded plugin.").Show(); _notificationService.CreateNotification().WithTitle("Reloaded plugin.").Show();
} }
public async Task InstallPrerequisites() public async Task ExecuteInstallPrerequisites()
{ {
List<IPrerequisitesSubject> subjects = new() {Plugin.Info}; List<IPrerequisitesSubject> subjects = new() {Plugin.Info};
subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled)); subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled));
@ -159,7 +165,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels
await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); 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}; List<IPrerequisitesSubject> subjects = new() {Plugin.Info};
subjects.AddRange(!forPluginRemoval ? Plugin.Features.Where(f => f.AlwaysEnabled) : Plugin.Features); 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}; List<IPrerequisitesSubject> subjects = new() {Plugin.Info};
subjects.AddRange(Plugin.Features); subjects.AddRange(Plugin.Features);
if (subjects.Any(s => s.Prerequisites.Any(p => p.UninstallActions.Any()))) if (subjects.Any(s => s.Prerequisites.Any(p => p.UninstallActions.Any())))
await RemovePrerequisites(true); await ExecuteRemovePrerequisites(true);
try try
{ {
@ -284,7 +290,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels
} }
} }
await Task.Run(() => await Task.Run(async () =>
{ {
try try
{ {
@ -292,10 +298,10 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels
} }
catch (Exception e) catch (Exception e)
{ {
_notificationService.CreateNotification() await Dispatcher.UIThread.InvokeAsync(() => _notificationService.CreateNotification()
.WithMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}") .WithMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}")
.HavingButton(b => b.WithText("View logs").WithAction(ShowLogsFolder)) .HavingButton(b => b.WithText("View logs").WithAction(ShowLogsFolder))
.Show(); .Show());
} }
finally finally
{ {

View File

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

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.UI.Avalonia.Screens.Plugins.ViewModels; using Artemis.UI.Avalonia.Screens.Plugins.ViewModels;
using Avalonia; using Avalonia;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
@ -19,22 +20,16 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.Views
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
ViewModel!.ConfigurationViewModel.CloseRequested += ConfigurationViewModelOnCloseRequested; Observable.FromEventPattern(
Disposable.Create(HandleDeactivation).DisposeWith(disposables); 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() private void InitializeComponent()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);