Console MessageBox, admin mode
This commit is contained in:
@@ -4,6 +4,7 @@ public class ConsoleApplicationConfiguration
|
||||
{
|
||||
public string? ConsoleDriver { get; set; }
|
||||
public bool DisableUtf8 { get; set; }
|
||||
public string? AdminModeIcon { get; set; }
|
||||
public string? ClipboardSingleIcon { get; set; }
|
||||
public string? ClipboardMultipleIcon { get; set; }
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||
<ProjectReference Include="..\..\AppCommon\FileTime.App.FrequencyNavigation.Abstractions\FileTime.App.FrequencyNavigation.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\Library\TerminalUI\TerminalUI.csproj" />
|
||||
<ProjectReference Include="..\..\Providers\FileTime.Providers.LocalAdmin.Abstractions\FileTime.Providers.LocalAdmin.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -7,6 +7,7 @@ using FileTime.App.FrequencyNavigation.ViewModels;
|
||||
using FileTime.ConsoleUI.App.Services;
|
||||
using FileTime.Core.Interactions;
|
||||
using FileTime.Core.Models;
|
||||
using FileTime.Providers.LocalAdmin;
|
||||
|
||||
namespace FileTime.ConsoleUI.App;
|
||||
|
||||
@@ -23,5 +24,6 @@ public interface IRootViewModel
|
||||
IFrequencyNavigationViewModel FrequencyNavigation { get; }
|
||||
IItemPreviewService ItemPreviewService { get; }
|
||||
IClipboardService ClipboardService { get; }
|
||||
IAdminElevationManager AdminElevationManager { get; }
|
||||
event Action<IInputElement>? FocusReadInputElement;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TerminalUI;
|
||||
using TerminalUI.ConsoleDrivers;
|
||||
using TerminalUI.Models;
|
||||
|
||||
namespace FileTime.ConsoleUI.App;
|
||||
|
||||
@@ -139,6 +140,14 @@ public class App : IApplication
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
_consoleDriver.ExitRestrictedMode();
|
||||
_consoleDriver.Clear();
|
||||
var size = _consoleDriver.GetWindowSize();
|
||||
var shutdownText = "Shutting down...";
|
||||
|
||||
_consoleDriver.SetCursorPosition(new Position(size.Width / 2 - shutdownText.Length / 2, size.Height / 2));
|
||||
_consoleDriver.Write(shutdownText);
|
||||
|
||||
Task.Run(async () => await _lifecycleService.ExitAsync()).Wait();
|
||||
}
|
||||
|
||||
|
||||
@@ -51,36 +51,49 @@ public class Dialogs
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateReadInputsFocus()
|
||||
{
|
||||
foreach (var readInputsChild in _readInputs.Children)
|
||||
{
|
||||
if (readInputsChild.DataContext == _inputElementToFocus)
|
||||
{
|
||||
if (FindFocusable(readInputsChild) is { } focusable)
|
||||
{
|
||||
focusable.Focus();
|
||||
_inputElementToFocus = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IFocusable? FindFocusable(IView view)
|
||||
{
|
||||
if (view is IFocusable focusable) return focusable;
|
||||
foreach (var viewVisualChild in view.VisualChildren)
|
||||
{
|
||||
if (FindFocusable(viewVisualChild) is { } focusableChild)
|
||||
return focusableChild;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public IView<IRootViewModel> View()
|
||||
{
|
||||
var root = new Grid<IRootViewModel>()
|
||||
{
|
||||
Margin = 5,
|
||||
ChildInitializer =
|
||||
{
|
||||
ReadInputs(),
|
||||
MessageBox()
|
||||
}
|
||||
};
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private IView<IRootViewModel> MessageBox()
|
||||
{
|
||||
var okButton = new Button<IRootViewModel>
|
||||
{
|
||||
Margin = "0 0 5 0",
|
||||
Content = new TextBlock<IRootViewModel>()
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
dc => dc.DialogService.LastMessageBox.Value.OkText,
|
||||
t => t.Text)),
|
||||
}.WithClickHandler(b => b.DataContext?.DialogService.LastMessageBox.Value?.Ok());
|
||||
|
||||
var cancelButton =
|
||||
new Button<IRootViewModel>
|
||||
{
|
||||
Margin = "0 0 5 0",
|
||||
Content = new TextBlock<IRootViewModel>()
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
dc => dc.DialogService.LastMessageBox.Value.CancelText,
|
||||
t => t.Text)),
|
||||
}
|
||||
.Setup(b => b.Bind(
|
||||
b,
|
||||
dc => dc.DialogService.LastMessageBox.Value.ShowCancel,
|
||||
b => b.IsVisible))
|
||||
.WithClickHandler(b => b.DataContext?.DialogService.LastMessageBox.Value?.Cancel());
|
||||
|
||||
var root = new Border<IRootViewModel>
|
||||
{
|
||||
Margin = 5,
|
||||
@@ -88,52 +101,53 @@ public class Dialogs
|
||||
Background = SpecialColor.None,
|
||||
Content = new Grid<IRootViewModel>
|
||||
{
|
||||
RowDefinitionsObject = "Auto Auto",
|
||||
ChildInitializer =
|
||||
{
|
||||
ReadInputs()
|
||||
new TextBlock<IRootViewModel>()
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
dc => dc.DialogService.LastMessageBox.Value.Text,
|
||||
t => t.Text)),
|
||||
new StackPanel<IRootViewModel>
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Extensions = {new GridPositionExtension(0, 1)},
|
||||
ChildInitializer =
|
||||
{
|
||||
okButton,
|
||||
cancelButton
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
root.Bind(
|
||||
root,
|
||||
d => d.DialogService.ReadInput.Value != null,
|
||||
v => v.IsVisible);
|
||||
d => d.DialogService.LastMessageBox.Value != null,
|
||||
v => v.IsVisible,
|
||||
fallbackValue: false);
|
||||
|
||||
((INotifyPropertyChanged) _readInputs).PropertyChanged += (_, e) =>
|
||||
((INotifyPropertyChanged) root).PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ItemsControl<object, object>.Children))
|
||||
if (e.PropertyName == nameof(IView.IsVisible))
|
||||
{
|
||||
_readInputChildHandlerUnSubscriber?.Invoke();
|
||||
|
||||
if (_readInputs.Children.Count > 0)
|
||||
{
|
||||
UpdateReadInputsFocus();
|
||||
}
|
||||
else
|
||||
{
|
||||
_inputElementToFocus = null;
|
||||
}
|
||||
|
||||
if (_readInputs.Children is INotifyCollectionChanged notifyCollectionChanged)
|
||||
{
|
||||
notifyCollectionChanged.CollectionChanged += NotifyCollectionChangedEventHandler;
|
||||
_readInputChildHandlerUnSubscriber = () => { notifyCollectionChanged.CollectionChanged -= NotifyCollectionChangedEventHandler; };
|
||||
}
|
||||
|
||||
void NotifyCollectionChangedEventHandler(
|
||||
object? sender,
|
||||
NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateReadInputsFocus();
|
||||
}
|
||||
okButton.Focus();
|
||||
}
|
||||
};
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private IView<IRootViewModel> ReadInputs()
|
||||
=> new Grid<IRootViewModel>
|
||||
{
|
||||
var root = new Border<IRootViewModel>
|
||||
{
|
||||
Margin = 5,
|
||||
BorderThickness = 1,
|
||||
Background = SpecialColor.None,
|
||||
Content = new Grid<IRootViewModel>
|
||||
{
|
||||
RowDefinitionsObject = "Auto Auto",
|
||||
ChildInitializer =
|
||||
@@ -150,8 +164,17 @@ public class Dialogs
|
||||
))
|
||||
.WithExtension(new GridPositionExtension(0, 1))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
root.Bind(
|
||||
root,
|
||||
d => d.DialogService.ReadInput.Value != null,
|
||||
v => v.IsVisible);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private IView<IPreviewElement> ReadInputPreviewItemTemplate()
|
||||
{
|
||||
var grid = new Grid<IPreviewElement>
|
||||
@@ -367,6 +390,64 @@ public class Dialogs
|
||||
|
||||
_readInputs = readInputs;
|
||||
|
||||
((INotifyPropertyChanged) _readInputs).PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ItemsControl<object, object>.Children))
|
||||
{
|
||||
_readInputChildHandlerUnSubscriber?.Invoke();
|
||||
|
||||
if (_readInputs.Children.Count > 0)
|
||||
{
|
||||
UpdateReadInputsFocus();
|
||||
}
|
||||
else
|
||||
{
|
||||
_inputElementToFocus = null;
|
||||
}
|
||||
|
||||
if (_readInputs.Children is INotifyCollectionChanged notifyCollectionChanged)
|
||||
{
|
||||
notifyCollectionChanged.CollectionChanged += NotifyCollectionChangedEventHandler;
|
||||
_readInputChildHandlerUnSubscriber = () => { notifyCollectionChanged.CollectionChanged -= NotifyCollectionChangedEventHandler; };
|
||||
}
|
||||
|
||||
void NotifyCollectionChangedEventHandler(
|
||||
object? sender,
|
||||
NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateReadInputsFocus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return readInputs;
|
||||
}
|
||||
|
||||
private void UpdateReadInputsFocus()
|
||||
{
|
||||
foreach (var readInputsChild in _readInputs.Children)
|
||||
{
|
||||
if (readInputsChild.DataContext == _inputElementToFocus)
|
||||
{
|
||||
if (FindFocusable(readInputsChild) is { } focusable)
|
||||
{
|
||||
focusable.Focus();
|
||||
_inputElementToFocus = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IFocusable? FindFocusable(IView view)
|
||||
{
|
||||
if (view is IFocusable focusable) return focusable;
|
||||
foreach (var viewVisualChild in view.VisualChildren)
|
||||
{
|
||||
if (FindFocusable(viewVisualChild) is { } focusableChild)
|
||||
return focusableChild;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,9 +138,19 @@ public class MainWindow
|
||||
{
|
||||
Margin = "2 0 0 0",
|
||||
Extensions = {new GridPositionExtension(2, 0)},
|
||||
Orientation = Orientation.Horizontal,
|
||||
ChildInitializer =
|
||||
{
|
||||
new TextBlock<IRootViewModel>
|
||||
{
|
||||
Text = _consoleApplicationConfiguration.Value.AdminModeIcon ??
|
||||
(_consoleApplicationConfiguration.Value.DisableUtf8 ? "A+ " : "\ud83d\udd11"),
|
||||
AsciiOnly = false
|
||||
}.Setup(t => t.Bind(
|
||||
t,
|
||||
dc => dc.AdminElevationManager.IsAdminInstanceRunning,
|
||||
t => t.IsVisible)),
|
||||
new TextBlock<IRootViewModel>
|
||||
{
|
||||
Text = _consoleApplicationConfiguration.Value.ClipboardSingleIcon ??
|
||||
(_consoleApplicationConfiguration.Value.DisableUtf8 ? "C " : "\ud83d\udccb"),
|
||||
|
||||
@@ -7,6 +7,7 @@ using FileTime.App.FrequencyNavigation.ViewModels;
|
||||
using FileTime.ConsoleUI.App.Services;
|
||||
using FileTime.Core.Interactions;
|
||||
using FileTime.Core.Models;
|
||||
using FileTime.Providers.LocalAdmin;
|
||||
|
||||
namespace FileTime.ConsoleUI.App;
|
||||
|
||||
@@ -20,6 +21,7 @@ public partial class RootViewModel : IRootViewModel
|
||||
public IFrequencyNavigationViewModel FrequencyNavigation { get; }
|
||||
public IItemPreviewService ItemPreviewService { get; }
|
||||
public IClipboardService ClipboardService { get; }
|
||||
public IAdminElevationManager AdminElevationManager { get; }
|
||||
public IDialogService DialogService { get; }
|
||||
public ITimelineViewModel TimelineViewModel { get; }
|
||||
public IDeclarativeProperty<VolumeSizeInfo?> VolumeSizeInfo { get;}
|
||||
@@ -34,7 +36,8 @@ public partial class RootViewModel : IRootViewModel
|
||||
ITimelineViewModel timelineViewModel,
|
||||
IFrequencyNavigationViewModel frequencyNavigation,
|
||||
IItemPreviewService itemPreviewService,
|
||||
IClipboardService clipboardService)
|
||||
IClipboardService clipboardService,
|
||||
IAdminElevationManager adminElevationManager)
|
||||
{
|
||||
AppState = appState;
|
||||
PossibleCommands = possibleCommands;
|
||||
@@ -44,6 +47,7 @@ public partial class RootViewModel : IRootViewModel
|
||||
FrequencyNavigation = frequencyNavigation;
|
||||
ItemPreviewService = itemPreviewService;
|
||||
ClipboardService = clipboardService;
|
||||
AdminElevationManager = adminElevationManager;
|
||||
|
||||
DialogService.ReadInput.PropertyChanged += (o, e) =>
|
||||
{
|
||||
|
||||
@@ -16,7 +16,12 @@ public class CustomLoggerSink : ILogEventSink
|
||||
|
||||
public void Emit(LogEvent logEvent)
|
||||
{
|
||||
if (logEvent.Level >= LogEventLevel.Error)
|
||||
if (logEvent.Level >= LogEventLevel.Error
|
||||
&& logEvent.Properties.TryGetValue("SourceContext", out var sourceContext))
|
||||
{
|
||||
var s = sourceContext.ToString();
|
||||
|
||||
if (s != "\"Microsoft.AspNetCore.SignalR.Client.HubConnection\"")
|
||||
{
|
||||
var message = logEvent.RenderMessage();
|
||||
if (logEvent.Exception is not null)
|
||||
@@ -26,3 +31,4 @@ public class CustomLoggerSink : ILogEventSink
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using FileTime.App.Core;
|
||||
using FileTime.App.Core.Configuration;
|
||||
using FileTime.ConsoleUI;
|
||||
@@ -19,6 +21,7 @@ Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
IConsoleDriver? driver = null;
|
||||
|
||||
#if DEBUG
|
||||
|
||||
(AppDataRoot, EnvironmentName) = Init.InitDevelopment();
|
||||
#endif
|
||||
if (AppDataRoot is null)
|
||||
@@ -49,6 +52,7 @@ try
|
||||
}
|
||||
finally
|
||||
{
|
||||
driver?.Clear();
|
||||
driver?.SetCursorVisible(true);
|
||||
driver?.Dispose();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"AdminModeIcon": "\uf0e7",
|
||||
"ClipboardSingleIcon": "\udb80\udd4c",
|
||||
"ClipboardMultipleIcon": "\udb84\ude68"
|
||||
}
|
||||
@@ -57,9 +57,9 @@ public static class Program
|
||||
|
||||
Log.Logger.Information("Early app starting...");
|
||||
|
||||
AppDomain.CurrentDomain.FirstChanceException -= OnFirstChanceException;
|
||||
AppDomain.CurrentDomain.UnhandledException -= OnAppDomainUnhandledException;
|
||||
TaskScheduler.UnobservedTaskException -= OnTaskSchedulerUnobservedTaskException;
|
||||
AppDomain.CurrentDomain.FirstChanceException += OnFirstChanceException;
|
||||
AppDomain.CurrentDomain.UnhandledException += OnAppDomainUnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += OnTaskSchedulerUnobservedTaskException;
|
||||
try
|
||||
{
|
||||
BuildAvaloniaApp()
|
||||
|
||||
@@ -11,6 +11,7 @@ public class DotnetDriver : IConsoleDriver
|
||||
public int ThreadId { get; set; }
|
||||
|
||||
public void EnterRestrictedMode() => CheckThreadId = true;
|
||||
public void ExitRestrictedMode() => CheckThreadId = false;
|
||||
|
||||
public virtual bool Init()
|
||||
{
|
||||
|
||||
@@ -24,4 +24,5 @@ public interface IConsoleDriver
|
||||
Size GetWindowSize();
|
||||
void Clear();
|
||||
void EnterRestrictedMode();
|
||||
void ExitRestrictedMode();
|
||||
}
|
||||
94
src/Library/TerminalUI/Controls/Button.cs
Normal file
94
src/Library/TerminalUI/Controls/Button.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using GeneralInputKey;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using TerminalUI.Color;
|
||||
using TerminalUI.ConsoleDrivers;
|
||||
using TerminalUI.Models;
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public partial class Button<T> : ContentView<Button<T>, T>, IFocusable
|
||||
{
|
||||
private record RenderState(IColor? Foreground, IColor? Background);
|
||||
|
||||
private Position? _cursorRenderPosition;
|
||||
private RenderState? _lastRenderState;
|
||||
|
||||
[Notify] private Position? _cursorPosition;
|
||||
private List<Action<Button<T>>> _clickHandlers = new();
|
||||
|
||||
public Button()
|
||||
{
|
||||
RerenderProperties.Add(nameof(CursorPosition));
|
||||
}
|
||||
|
||||
public void Focus()
|
||||
=> ApplicationContext?.FocusManager.SetFocus(this);
|
||||
|
||||
public void UnFocus()
|
||||
=> ApplicationContext?.FocusManager.UnFocus(this);
|
||||
|
||||
public void SetCursorPosition(IConsoleDriver consoleDriver)
|
||||
{
|
||||
if (_cursorRenderPosition is null) return;
|
||||
consoleDriver.SetCursorPosition(_cursorRenderPosition.Value);
|
||||
}
|
||||
|
||||
protected override Size CalculateSize()
|
||||
{
|
||||
if (Content is null || !Content.IsVisible) return new Size(0, 0);
|
||||
|
||||
return Content.GetRequestedSize();
|
||||
}
|
||||
|
||||
protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size)
|
||||
{
|
||||
if (ContentRendererMethod is null)
|
||||
{
|
||||
throw new NullReferenceException(
|
||||
nameof(ContentRendererMethod)
|
||||
+ " is null, cannot render content of "
|
||||
+ Content?.GetType().Name
|
||||
+ " with DataContext of "
|
||||
+ DataContext?.GetType().Name);
|
||||
}
|
||||
|
||||
var backgroundColor = Background ?? renderContext.Background;
|
||||
var foregroundColor = Foreground ?? renderContext.Foreground;
|
||||
|
||||
var renderState = new RenderState(foregroundColor, backgroundColor);
|
||||
|
||||
var forceRerender = !renderContext.ForceRerender && !NeedsRerender(renderState);
|
||||
_lastRenderState = renderState;
|
||||
|
||||
var childRenderContext = renderContext with
|
||||
{
|
||||
Background = backgroundColor,
|
||||
Foreground = foregroundColor,
|
||||
ForceRerender = forceRerender
|
||||
};
|
||||
|
||||
_cursorRenderPosition = position;
|
||||
|
||||
return ContentRendererMethod(childRenderContext, position, size);
|
||||
}
|
||||
|
||||
private bool NeedsRerender(RenderState renderState) => renderState != _lastRenderState;
|
||||
|
||||
public override void HandleKeyInput(GeneralKeyEventArgs keyEventArgs)
|
||||
{
|
||||
if (keyEventArgs.Key != Keys.Enter) return;
|
||||
|
||||
keyEventArgs.Handled = true;
|
||||
foreach (var clickHandler in _clickHandlers)
|
||||
{
|
||||
clickHandler(this);
|
||||
}
|
||||
}
|
||||
|
||||
public Button<T> WithClickHandler(Action<Button<T>> handler)
|
||||
{
|
||||
_clickHandlers.Add(handler);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -125,8 +125,7 @@ public sealed partial class ItemsControl<TDataContext, TItem>
|
||||
{
|
||||
if (!child.IsVisible) continue;
|
||||
|
||||
if (!_requestedSizes.TryGetValue(child, out var childSize)) throw new Exception("Child size not found");
|
||||
|
||||
if (!_requestedSizes.TryGetValue(child, out var childSize)) continue;
|
||||
|
||||
var childPosition = Orientation == Orientation.Vertical
|
||||
? position with {Y = position.Y + delta}
|
||||
|
||||
@@ -8,6 +8,7 @@ public class EventLoop : IEventLoop
|
||||
private readonly ILogger<EventLoop> _logger;
|
||||
private readonly List<Action> _initializers = new();
|
||||
private readonly List<Action> _permanentQueue = new();
|
||||
private readonly List<Action> _finalizers = new();
|
||||
|
||||
public int ThreadId { get; set; } = -1;
|
||||
|
||||
@@ -21,6 +22,7 @@ public class EventLoop : IEventLoop
|
||||
|
||||
public void AddToPermanentQueue(Action action) => _permanentQueue.Add(action);
|
||||
public void AddInitializer(Action action) => _initializers.Add(action);
|
||||
public void AddFinalizer(Action action) => _finalizers.Add(action);
|
||||
|
||||
public void Run()
|
||||
{
|
||||
@@ -35,6 +37,10 @@ public class EventLoop : IEventLoop
|
||||
ProcessQueues();
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
foreach (var finalizer in _finalizers)
|
||||
{
|
||||
finalizer();
|
||||
}
|
||||
ThreadId = -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,4 +6,5 @@ public interface IEventLoop
|
||||
void AddToPermanentQueue(Action action);
|
||||
void AddInitializer(Action action);
|
||||
int ThreadId { get; set; }
|
||||
void AddFinalizer(Action action);
|
||||
}
|
||||
@@ -33,6 +33,10 @@ public class RenderEngine : IRenderEngine
|
||||
_applicationContext.ConsoleDriver.ThreadId = _eventLoop.ThreadId;
|
||||
_applicationContext.ConsoleDriver.EnterRestrictedMode();
|
||||
});
|
||||
_eventLoop.AddFinalizer(() =>
|
||||
{
|
||||
_applicationContext.ConsoleDriver.ExitRestrictedMode();
|
||||
});
|
||||
}
|
||||
|
||||
public void RequestRerender(IView view) => RequestRerender();
|
||||
|
||||
Reference in New Issue
Block a user