我认为很稳定了,后面就要开始弄插件不稳定了
This commit is contained in:
lincube
2026-03-12 12:25:22 +08:00
parent 6952cb2c3e
commit 4679ee006f
20 changed files with 1197 additions and 205 deletions

View File

@@ -6,21 +6,27 @@ using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Styling;
using AvaloniaWebView;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
using WebViewCore.Events;
namespace LanMountainDesktop.Views.Components;
public partial class BrowserWidget : UserControl, IDesktopComponentWidget
, IDesktopPageVisibilityAwareComponentWidget, IDisposable
public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
IDesktopPageVisibilityAwareComponentWidget, IComponentPlacementContextAware, IDisposable
{
private static readonly Uri DefaultHomeUri = new("https://www.bing.com");
private double _currentCellSize = 48;
private string _componentId = BuiltInComponentIds.DesktopBrowser;
private string _placementId = string.Empty;
private bool? _isNightModeApplied;
private Uri _lastKnownUri = DefaultHomeUri;
private bool _isOnActiveDesktopPage;
private bool _isAttachedToVisualTree;
private bool _isEditMode;
private bool _isWebViewActive = true;
private bool _isWebViewFaulted;
private readonly WebView2RuntimeAvailability _runtimeAvailability;
private bool _isDisposed;
@@ -45,8 +51,8 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
ApplyRuntimeUnavailableState();
}
AddressTextBox.Text = DefaultHomeUri.ToString();
UpdateWebViewActiveState();
NavigateTo(DefaultHomeUri);
}
public void Dispose()
@@ -74,17 +80,15 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
_currentCellSize = Math.Max(1, cellSize);
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(_currentCellSize * 0.34, 12, 28));
RootBorder.Padding = new Thickness(
Math.Clamp(_currentCellSize * 0.20, 8, 18));
RootBorder.Padding = new Thickness(Math.Clamp(_currentCellSize * 0.20, 8, 18));
WebViewHostBorder.CornerRadius = new CornerRadius(Math.Clamp(_currentCellSize * 0.24, 10, 22));
AddressBarBorder.CornerRadius = new CornerRadius(Math.Clamp(_currentCellSize * 0.22, 10, 20));
AddressBarBorder.Padding = new Thickness(8, 6);
var rowSpacing = 8d;
if (RootBorder.Child is Grid rootGrid)
{
rootGrid.RowSpacing = rowSpacing;
rootGrid.RowSpacing = 8d;
}
var buttonSize = Math.Clamp(_currentCellSize * 0.72, 30, 36);
@@ -111,16 +115,33 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
AddressTextBox.Height = buttonSize;
}
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{
_isOnActiveDesktopPage = isOnActivePage;
_isEditMode = isEditMode;
UpdateWebViewActiveState();
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopBrowser
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttachedToVisualTree = true;
ApplyTheme(force: true);
UpdateWebViewActiveState();
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttachedToVisualTree = false;
_isOnActiveDesktopPage = false;
UpdateWebViewActiveState();
DeactivateWebView(clearUrl: false);
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
@@ -202,28 +223,20 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
private void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
{
if (!_runtimeAvailability.IsAvailable)
if (!CanUseWebView())
{
return;
}
if (!_isWebViewActive)
if (!TryReloadWebView("Refresh"))
{
return;
TryNavigate(DefaultHomeUri, "RefreshFallback");
}
if (BrowserWebView.Url is not null)
{
BrowserWebView.Reload();
return;
}
NavigateTo(DefaultHomeUri);
}
private void OnGoButtonClick(object? sender, RoutedEventArgs e)
{
if (!_runtimeAvailability.IsAvailable)
if (!CanUseWebView())
{
return;
}
@@ -233,7 +246,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
private void OnAddressTextBoxKeyDown(object? sender, KeyEventArgs e)
{
if (!_runtimeAvailability.IsAvailable)
if (!CanUseWebView())
{
return;
}
@@ -249,7 +262,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
private void NavigateFromAddressBar()
{
if (!_runtimeAvailability.IsAvailable)
if (!CanUseWebView())
{
return;
}
@@ -269,7 +282,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
AddressTextBox.Text = uri.ToString();
if (_isWebViewActive)
{
BrowserWebView.Url = uri;
TryNavigate(uri, "NavigateTo");
}
}
@@ -284,25 +297,16 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
AddressTextBox.Text = e.Url.ToString();
}
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{
_isOnActiveDesktopPage = isOnActivePage;
_isEditMode = isEditMode;
UpdateWebViewActiveState();
}
private void UpdateWebViewActiveState()
{
if (!_runtimeAvailability.IsAvailable)
if (!_runtimeAvailability.IsAvailable || _isWebViewFaulted)
{
_isWebViewActive = false;
BrowserWebView.Url = null;
BrowserWebView.IsVisible = false;
BrowserWebView.IsHitTestVisible = false;
ApplyRuntimeUnavailableState();
return;
}
var shouldBeActive = _isOnActiveDesktopPage && !_isEditMode && IsVisible;
var shouldBeActive = _isAttachedToVisualTree && _isOnActiveDesktopPage && !_isEditMode && IsVisible;
if (_isWebViewActive == shouldBeActive)
{
return;
@@ -311,40 +315,118 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget
_isWebViewActive = shouldBeActive;
if (!_isWebViewActive)
{
if (BrowserWebView.Url is Uri currentUri)
{
_lastKnownUri = currentUri;
}
DeactivateWebView(clearUrl: false);
return;
}
BrowserWebView.IsHitTestVisible = false;
BrowserWebView.IsVisible = false;
BrowserWebView.Url = null;
ActivateWebView();
}
private void ActivateWebView()
{
if (_isWebViewFaulted || !_runtimeAvailability.IsAvailable)
{
ApplyRuntimeUnavailableState();
return;
}
BrowserWebView.IsVisible = true;
BrowserWebView.IsHitTestVisible = true;
BrowserWebView.Url = _lastKnownUri;
RefreshButton.IsEnabled = true;
GoButton.IsEnabled = true;
AddressTextBox.IsEnabled = true;
UnavailableOverlay.IsVisible = false;
TryNavigate(_lastKnownUri, "Activate");
}
private void DeactivateWebView(bool clearUrl)
{
BrowserWebView.IsHitTestVisible = false;
BrowserWebView.IsVisible = false;
if (clearUrl)
{
TryClearWebViewUrl();
}
}
private bool TryReloadWebView(string action)
{
try
{
BrowserWebView.Reload();
return true;
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
EnterFaultedState(action, ex);
return false;
}
}
private bool TryNavigate(Uri uri, string action)
{
try
{
BrowserWebView.Url = uri;
return true;
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
EnterFaultedState(action, ex);
return false;
}
}
private void TryClearWebViewUrl()
{
try
{
BrowserWebView.Url = null;
}
catch
{
// Best-effort cleanup only.
}
}
private bool CanUseWebView()
{
return _runtimeAvailability.IsAvailable && !_isWebViewFaulted && _isWebViewActive;
}
private void ApplyRuntimeUnavailableState()
{
_isWebViewActive = false;
BrowserWebView.Url = null;
BrowserWebView.IsVisible = false;
BrowserWebView.IsHitTestVisible = false;
RefreshButton.IsEnabled = false;
GoButton.IsEnabled = false;
AddressTextBox.IsEnabled = false;
AddressTextBox.Text = string.Empty;
AddressTextBox.Text = _lastKnownUri.ToString();
UnavailableMessageTextBlock.Text = string.IsNullOrWhiteSpace(_runtimeAvailability.Message)
? "WebView runtime unavailable."
: _runtimeAvailability.Message;
UnavailableMessageTextBlock.Text = _isWebViewFaulted
? "The browser component is temporarily unavailable. Restart the app to retry."
: string.IsNullOrWhiteSpace(_runtimeAvailability.Message)
? "WebView runtime unavailable."
: _runtimeAvailability.Message;
UnavailableOverlay.IsVisible = true;
}
private void EnterFaultedState(string action, Exception ex)
{
_isWebViewFaulted = true;
_isWebViewActive = false;
AppLogger.Warn(
"BrowserWidget",
$"Browser component faulted. Action={action}; ComponentId={_componentId}; PlacementId={_placementId}; RuntimeAvailability={_runtimeAvailability.IsAvailable}; RuntimeVersion={_runtimeAvailability.Version ?? string.Empty}; CurrentUrl={_lastKnownUri}",
ex);
TryClearWebViewUrl();
ApplyRuntimeUnavailableState();
}
private static Uri? TryNormalizeUri(string? rawText)
{
if (string.IsNullOrWhiteSpace(rawText))

View File

@@ -0,0 +1,226 @@
using System;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
internal sealed class DesktopComponentFailureView : UserControl, IDesktopComponentWidget
{
private readonly Border _rootBorder;
private readonly TextBlock _titleBlock;
private readonly TextBlock _summaryBlock;
private readonly TextBlock _statusBlock;
private readonly Button _toggleDetailsButton;
private readonly Button _copyReportButton;
private readonly Border _detailsBorder;
private readonly TextBox _reportTextBox;
private readonly string _componentId;
private readonly string? _placementId;
private readonly string _reportText;
private bool _detailsVisible;
public DesktopComponentFailureView(
string componentName,
string componentId,
string? placementId,
int? pageIndex,
string action,
Exception exception)
{
_componentId = componentId;
_placementId = placementId;
_reportText = BuildReport(componentName, componentId, placementId, pageIndex, action, exception);
_titleBlock = new TextBlock
{
Text = string.IsNullOrWhiteSpace(componentName) ? "组件暂时不可用" : componentName,
FontWeight = FontWeight.SemiBold,
TextWrapping = TextWrapping.Wrap
};
_summaryBlock = new TextBlock
{
Text = "该组件已临时停用,并由信息占位保留原位置。你可以展开详情或复制错误报告。",
Foreground = CreateBrush("#FFD6DEE9"),
TextWrapping = TextWrapping.Wrap
};
_statusBlock = new TextBlock
{
IsVisible = false,
Foreground = CreateBrush("#FF93C5FD"),
TextWrapping = TextWrapping.Wrap
};
_toggleDetailsButton = CreateButton("查看错误信息", OnToggleDetailsClick);
_copyReportButton = CreateButton("复制错误报告", OnCopyReportClick);
_reportTextBox = new TextBox
{
Text = _reportText,
IsReadOnly = true,
AcceptsReturn = true,
TextWrapping = TextWrapping.Wrap,
MinHeight = 96,
MaxHeight = 220,
Background = CreateBrush("#CC0F172A"),
Foreground = CreateBrush("#FFE2E8F0"),
BorderThickness = new Thickness(0),
Padding = new Thickness(8)
};
_detailsBorder = new Border
{
IsVisible = false,
Background = CreateBrush("#660F172A"),
BorderBrush = CreateBrush("#33475569"),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Child = _reportTextBox
};
_rootBorder = new Border
{
Background = CreateBrush("#D91E293B"),
BorderBrush = CreateBrush("#336B7280"),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(18),
Padding = new Thickness(14),
ClipToBounds = true,
Child = new StackPanel
{
Spacing = 8,
VerticalAlignment = VerticalAlignment.Center,
Children =
{
_titleBlock,
_summaryBlock,
new WrapPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Left,
ItemSpacing = 8,
LineSpacing = 8,
Children =
{
_toggleDetailsButton,
_copyReportButton
}
},
_statusBlock,
_detailsBorder
}
}
};
Content = _rootBorder;
ApplyCellSize(48);
}
public void ApplyCellSize(double cellSize)
{
var normalized = Math.Max(1, cellSize);
_rootBorder.CornerRadius = new CornerRadius(Math.Clamp(normalized * 0.24, 12, 24));
_rootBorder.Padding = new Thickness(Math.Clamp(normalized * 0.24, 10, 18));
_titleBlock.FontSize = Math.Clamp(normalized * 0.36, 14, 22);
_summaryBlock.FontSize = Math.Clamp(normalized * 0.24, 11, 15);
_statusBlock.FontSize = Math.Clamp(normalized * 0.22, 10, 13);
_toggleDetailsButton.FontSize = Math.Clamp(normalized * 0.22, 10, 14);
_copyReportButton.FontSize = Math.Clamp(normalized * 0.22, 10, 14);
_toggleDetailsButton.Padding = new Thickness(Math.Clamp(normalized * 0.18, 8, 12), 6);
_copyReportButton.Padding = new Thickness(Math.Clamp(normalized * 0.18, 8, 12), 6);
_reportTextBox.FontSize = Math.Clamp(normalized * 0.2, 10, 13);
_reportTextBox.MaxHeight = Math.Clamp(normalized * 5.2, 120, 260);
}
private static Button CreateButton(string text, EventHandler<RoutedEventArgs> clickHandler)
{
var button = new Button
{
Content = text,
Background = CreateBrush("#80334155"),
Foreground = Brushes.White,
BorderBrush = CreateBrush("#335B6575"),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(999),
HorizontalAlignment = HorizontalAlignment.Left
};
button.Click += clickHandler;
return button;
}
private void OnToggleDetailsClick(object? sender, RoutedEventArgs e)
{
_detailsVisible = !_detailsVisible;
_detailsBorder.IsVisible = _detailsVisible;
_toggleDetailsButton.Content = _detailsVisible ? "隐藏错误信息" : "查看错误信息";
UpdateStatus(null);
}
private void OnCopyReportClick(object? sender, RoutedEventArgs e)
{
UiExceptionGuard.FireAndForgetGuarded(
CopyReportAsync,
"DesktopComponentFailureView.CopyReport",
UiExceptionGuard.BuildContext(
("ComponentId", _componentId),
("PlacementId", _placementId)));
}
private async Task CopyReportAsync()
{
var topLevel = TopLevel.GetTopLevel(this);
var clipboard = topLevel?.Clipboard;
if (clipboard is null)
{
UpdateStatus("当前环境不支持复制错误报告。");
return;
}
await clipboard.SetTextAsync(_reportText);
UpdateStatus("错误报告已复制到剪贴板。");
}
private void UpdateStatus(string? message)
{
_statusBlock.Text = message ?? string.Empty;
_statusBlock.IsVisible = !string.IsNullOrWhiteSpace(message);
}
private static string BuildReport(
string componentName,
string componentId,
string? placementId,
int? pageIndex,
string action,
Exception exception)
{
var version = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "unknown";
var builder = new StringBuilder();
builder.AppendLine("LanMountainDesktop Component Failure Report");
builder.AppendLine($"GeneratedAt: {DateTimeOffset.Now:O}");
builder.AppendLine($"AppVersion: {version}");
builder.AppendLine($"Action: {action}");
builder.AppendLine($"ComponentName: {componentName}");
builder.AppendLine($"ComponentId: {componentId}");
builder.AppendLine($"PlacementId: {placementId ?? string.Empty}");
builder.AppendLine($"PageIndex: {pageIndex?.ToString() ?? string.Empty}");
builder.AppendLine($"ExceptionType: {exception.GetType().FullName}");
builder.AppendLine($"ExceptionMessage: {exception.Message}");
builder.AppendLine();
builder.AppendLine(exception.ToString());
return builder.ToString();
}
private static IBrush CreateBrush(string colorHex)
{
return new SolidColorBrush(Color.Parse(colorHex));
}
}

View File

@@ -1522,7 +1522,7 @@ public partial class MainWindow
placement.PlacementId = Guid.NewGuid().ToString("N");
}
var component = CreateDesktopComponentControl(placement.ComponentId, placement.PlacementId);
var component = CreateDesktopComponentControl(placement.ComponentId, placement.PlacementId, placement.PageIndex);
if (component is null)
{
return null;
@@ -1956,23 +1956,54 @@ public partial class MainWindow
return onLeft || onRight || onTop || onBottom;
}
private Control? CreateDesktopComponentControl(string componentId, string? placementId = null)
private Control? CreateDesktopComponentControl(string componentId, string? placementId = null, int? pageIndex = null)
{
if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
{
return null;
}
var component = runtimeDescriptor.CreateControl(
_currentDesktopCellSize,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_componentSettingsService,
placementId);
component.Classes.Add(DesktopComponentClass);
return component;
return CreateDesktopComponentControl(runtimeDescriptor, _currentDesktopCellSize, placementId, pageIndex, "DesktopSurface");
}
private Control? CreateDesktopComponentControl(
DesktopComponentRuntimeDescriptor runtimeDescriptor,
double cellSize,
string? placementId,
int? pageIndex,
string action)
{
try
{
var component = runtimeDescriptor.CreateControl(
cellSize,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_componentSettingsService,
placementId);
component.Classes.Add(DesktopComponentClass);
return component;
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
AppLogger.Warn(
"ComponentRuntime",
$"Action={action}; ComponentId={runtimeDescriptor.Definition.Id}; PlacementId={placementId ?? string.Empty}; PageIndex={pageIndex?.ToString() ?? string.Empty}; ExceptionType={ex.GetType().FullName}; IsFatal=false",
ex);
var failureView = new DesktopComponentFailureView(
runtimeDescriptor.Definition.DisplayName,
runtimeDescriptor.Definition.Id,
placementId,
pageIndex,
action,
ex);
failureView.ApplyCellSize(cellSize);
failureView.Classes.Add(DesktopComponentClass);
return failureView;
}
}
private void CollapseComponentLibraryPanel()
@@ -3113,13 +3144,16 @@ public partial class MainWindow
var previewHeight = previewSpan.HeightCells * previewCellSize;
var renderCellSize = Math.Clamp(previewCellSize * 1.15, 26, 110);
var previewControl = descriptor.CreateControl(
var previewControl = CreateDesktopComponentControl(
descriptor,
renderCellSize,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_componentSettingsService);
placementId: null,
pageIndex: null,
action: "ComponentLibraryPreview");
if (previewControl is null)
{
continue;
}
// Component library previews must stay non-interactive so drag gesture is reliable.
previewControl.IsHitTestVisible = false;
previewControl.Focusable = false;

View File

@@ -1,59 +1,58 @@
using System;
using Avalonia.Interactivity;
using System.Threading.Tasks;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private readonly DispatcherTimer _singleInstanceNoticeTimer = new()
{
Interval = TimeSpan.FromSeconds(6)
};
private bool _isSingleInstancePromptVisible;
internal void ShowSingleInstanceNotice()
{
void ShowPrompt()
{
UiExceptionGuard.FireAndForgetGuarded(
ShowSingleInstanceNoticeCoreAsync,
"MainWindow.ShowSingleInstanceNotice");
}
if (Dispatcher.UIThread.CheckAccess())
{
ShowSingleInstanceNoticeCore();
ShowPrompt();
return;
}
Dispatcher.UIThread.Post(ShowSingleInstanceNoticeCore, DispatcherPriority.Send);
Dispatcher.UIThread.Post(ShowPrompt, DispatcherPriority.Send);
}
private void ShowSingleInstanceNoticeCore()
private async Task ShowSingleInstanceNoticeCoreAsync()
{
SingleInstanceNoticeTitleTextBlock.Text = L(
"single_instance.notice.title",
"App already open");
SingleInstanceNoticeDescriptionTextBlock.Text = L(
"single_instance.notice.description",
"LanMountainDesktop is already running. Switched back to the active desktop.");
SingleInstanceNoticeButtonTextBlock.Text = L(
"single_instance.notice.button",
"Got it");
SingleInstanceNoticeDock.IsVisible = true;
if (_isSingleInstancePromptVisible)
{
return;
}
_singleInstanceNoticeTimer.Stop();
_singleInstanceNoticeTimer.Tick -= OnSingleInstanceNoticeTimerTick;
_singleInstanceNoticeTimer.Tick += OnSingleInstanceNoticeTimerTick;
_singleInstanceNoticeTimer.Start();
}
_isSingleInstancePromptVisible = true;
private void OnSingleInstanceNoticeButtonClick(object? sender, RoutedEventArgs e)
{
HideSingleInstanceNotice();
}
try
{
var dialog = new ContentDialog
{
Title = L("single_instance.notice.title", "应用已经运行"),
Content = L(
"single_instance.notice.description",
"应用已经运行,无需多次点击打开。"),
PrimaryButtonText = L("single_instance.notice.button", "确定"),
DefaultButton = ContentDialogButton.Primary
};
private void OnSingleInstanceNoticeTimerTick(object? sender, EventArgs e)
{
HideSingleInstanceNotice();
}
private void HideSingleInstanceNotice()
{
_singleInstanceNoticeTimer.Stop();
SingleInstanceNoticeDock.IsVisible = false;
await dialog.ShowAsync(this);
}
finally
{
_isSingleInstancePromptVisible = false;
}
}
}

View File

@@ -471,51 +471,6 @@
<StackPanel Grid.Row="1"
Spacing="12">
<Border x:Name="SingleInstanceNoticeDock"
IsVisible="False"
Classes="glass-panel"
CornerRadius="18"
Padding="14,12">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="12">
<Border Width="34"
Height="34"
CornerRadius="17"
Background="{DynamicResource AdaptiveAccentBrush}">
<fi:FluentIcon Icon="Alert"
IconVariant="Regular"
FontSize="16"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1"
Spacing="2"
VerticalAlignment="Center">
<TextBlock x:Name="SingleInstanceNoticeTitleTextBlock"
FontSize="13"
FontWeight="SemiBold"
Text="App already open" />
<TextBlock x:Name="SingleInstanceNoticeDescriptionTextBlock"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="LanMountainDesktop is already running. Switched back to the active desktop." />
</StackPanel>
<Button x:Name="SingleInstanceNoticeButton"
Grid.Column="2"
Padding="14,8"
Click="OnSingleInstanceNoticeButtonClick">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:FluentIcon Icon="Checkmark"
IconVariant="Regular" />
<TextBlock x:Name="SingleInstanceNoticeButtonTextBlock"
VerticalAlignment="Center"
Text="Got it" />
</StackPanel>
</Button>
</Grid>
</Border>
<Border x:Name="PendingRestartDock"
IsVisible="False"
Classes="glass-panel"